VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
CI / test-bookworm (push) Failing after 11s
CI / test-latest (push) Successful in 1m27s

Chromium kiosk without a desktop for the Raspberry Pi 3, a Flask web
interface on port 8080 (kiosk, system and network management), a
restricted root helper, network changes with automatic rollback, a
healthcheck timer, install/update/uninstall scripts and a pi-gen image
build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rothmanandClaude Opus 5.5 committed 2026-11-04 21:38:49 +01:00
commit a33c4822bf
96 files changed
+7535

No files matched your search

@@ -0,0 +1,17 @@
<fieldset class="radio-row">
<legend>IPv4-adres</legend>
<label class="check"><input type="radio" name="method" value="dhcp" {% if p.method != 'static' %}checked{% endif %}> Automatisch (DHCP)</label>
<label class="check"><input type="radio" name="method" value="static" {% if p.method == 'static' %}checked{% endif %}> Statisch</label>
</fieldset>
<div class="static-fields" data-static-fields>
<label>IP-adres met prefix
<input type="text" name="address" value="{{ p.address }}" placeholder="192.168.1.50/24" autocomplete="off" spellcheck="false">
</label>
<label>Gateway
<input type="text" name="gateway" value="{{ p.gateway }}" placeholder="192.168.1.1" autocomplete="off" spellcheck="false">
</label>
</div>
<label>DNS-servers
<input type="text" name="dns" value="{{ p.dns }}" placeholder="Leeg = via DHCP, bijv. 1.1.1.1, 9.9.9.9" autocomplete="off" spellcheck="false">
<small>Maximaal drie, gescheiden door komma's. Verplicht bij een statisch adres.</small>
</label>
@@ -0,0 +1,19 @@
{% macro badge(status, label, field=None) -%}
<span class="badge badge-{{ status }}" {% if field %}data-badge="{{ field }}"{% endif %}><span class="dot" aria-hidden="true"></span><span data-badge-label>{{ label }}</span></span>
{%- endmacro %}
{% macro action(url, label, cls="btn-primary", confirm=None, confirm_title=None, big=False) -%}
<form method="post" action="{{ url }}" class="inline-form" {% if confirm %}data-confirm="{{ confirm }}" data-confirm-title="{{ confirm_title or label }}"{% endif %}>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn {{ cls }}{% if big %} btn-big{% endif %}">{{ label }}</button>
</form>
{%- endmacro %}
{% macro csrf() -%}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{%- endmacro %}
{# Width is applied by app.js: the CSP does not allow inline style attributes. #}
{% macro meter(percent, field) -%}
<div class="meter" role="presentation"><span data-meter="{{ field }}" data-pct="{{ percent }}"></span></div>
{%- endmacro %}
+64
View File
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}{{ title or "Beheer" }}{% endblock %} · VoxIP Kiosk Manager</title>
<link rel="icon" href="{{ url_for('static', filename='logo.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}?v={{ version }}">
<script src="{{ url_for('static', filename='app.js') }}?v={{ version }}" defer></script>
</head>
<body>
<header class="topbar">
<div class="topbar-inner">
<a class="brand" href="{{ url_for('ui.dashboard') }}">
<img src="{{ url_for('static', filename='logo.svg') }}" alt="" width="28" height="28">
<span><strong>VoxIP</strong> Kiosk Manager</span>
</a>
{% if logged_in %}
<button class="nav-toggle" type="button" aria-controls="mainnav" aria-expanded="false" data-nav-toggle>
<span class="sr-only">Menu</span><span class="bars" aria-hidden="true"></span>
</button>
<nav id="mainnav" class="mainnav">
<a href="{{ url_for('ui.dashboard') }}" {% if active == 'dashboard' %}aria-current="page"{% endif %}>Dashboard</a>
<a href="{{ url_for('ui.kiosk') }}" {% if active == 'kiosk' %}aria-current="page"{% endif %}>Kiosk</a>
<a href="{{ url_for('ui.system') }}" {% if active == 'system' %}aria-current="page"{% endif %}>Systeem</a>
<a href="{{ url_for('ui.network') }}" {% if active == 'network' %}aria-current="page"{% endif %}>Netwerk</a>
<a href="{{ url_for('ui.logs') }}" {% if active == 'logs' %}aria-current="page"{% endif %}>Logboek</a>
<a href="{{ url_for('ui.change_password') }}" {% if active == 'account' %}aria-current="page"{% endif %}>Wachtwoord</a>
<form method="post" action="{{ url_for('ui.logout') }}" class="nav-logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Uitloggen</button>
</form>
</nav>
{% endif %}
</div>
</header>
<main class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="status">{{ message }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="footer">
<span>{{ device_name }} · {{ hostname }}</span>
<span>VoxIP Kiosk OS {{ version }}</span>
</footer>
<dialog id="confirm-dialog" class="dialog">
<form method="dialog">
<h2 data-confirm-title>Weet je het zeker?</h2>
<p data-confirm-text></p>
<div class="dialog-actions">
<button value="cancel" class="btn btn-secondary">Annuleren</button>
<button value="ok" class="btn btn-danger" data-confirm-ok>Doorgaan</button>
</div>
</form>
</dialog>
</body>
</html>
@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% from "_macros.html" import badge, action, meter with context %}
{% set title = "Dashboard" %}
{% block content %}
<div class="page-head">
<h1>Dashboard</h1>
<p class="muted">Ververst automatisch elke 5 seconden.</p>
</div>
<section class="actions-row" aria-label="Snelle acties">
{{ action(url_for('ui.kiosk_reload'), "Pagina herladen", "btn-primary", big=True) }}
{{ action(url_for('ui.kiosk_restart'), "Chromium herstarten", "btn-warning", confirm="Chromium wordt afgesloten en opnieuw gestart. Het scherm is enkele seconden zwart.", big=True) }}
{{ action(url_for('ui.system_reboot'), "Raspberry Pi herstarten", "btn-danger", confirm="De Raspberry Pi herstart. Het scherm en de beheerinterface zijn ongeveer een minuut niet beschikbaar.", big=True) }}
</section>
<div class="grid" data-live-status="{{ url_for('ui.api_status') }}">
<section class="card">
<h2>Kiosk</h2>
<dl class="kv">
<dt>Chromium</dt><dd>{{ badge(s.browser.status, s.browser.label, "browser") }}</dd>
<dt>Kiosk-URL</dt><dd class="break" data-field="url">{{ s.url }}</dd>
<dt>Verbinding</dt><dd>{{ badge(s.network.status, s.network.label, "network") }}</dd>
<dt>Herstarts</dt><dd data-field="browser.restarts">{{ s.browser.restarts }}</dd>
</dl>
{% if s.last_error %}
<p class="note">Laatste melding: <span data-field="last_error.message">{{ s.last_error.message }}</span></p>
{% endif %}
</section>
<section class="card">
<h2>Systeem</h2>
<dl class="kv">
<dt>Uptime</dt><dd data-field="uptime">{{ s.uptime }}</dd>
<dt>CPU-belasting</dt><dd><span data-field="cpu">{{ s.cpu }}</span> <span class="muted">(load <span data-field="load">{{ s.load }}</span>)</span></dd>
<dt>Temperatuur</dt><dd>{{ badge(s.temperature_status, s.temperature, "temperature") }}</dd>
{% if s.power %}<dt>Voeding</dt><dd>{{ badge(s.power.status, s.power.label, "power") }}</dd>{% endif %}
</dl>
</section>
<section class="card">
<h2>Geheugen &amp; opslag</h2>
<dl class="kv">
<dt>RAM</dt><dd><span data-field="memory">{{ s.memory }}</span>{{ meter(s.memory_percent, "memory_percent") }}</dd>
<dt>SD-kaart</dt><dd><span data-field="disk">{{ s.disk }}</span>{{ meter(s.disk_percent, "disk_percent") }}</dd>
</dl>
</section>
<section class="card">
<h2>Netwerk</h2>
<dl class="kv">
<dt>Ethernet</dt><dd data-field="eth0">{{ s.eth0 }}</dd>
<dt>Wifi</dt><dd data-field="wlan0">{{ s.wlan0 }}</dd>
<dt>Hostname</dt><dd>{{ hostname }}.local</dd>
</dl>
</section>
<section class="card">
<h2>Over dit apparaat</h2>
<dl class="kv">
<dt>Naam</dt><dd>{{ device_name }}</dd>
<dt>Versie</dt><dd>VoxIP Kiosk OS {{ s.version }}</dd>
<dt>Besturingssysteem</dt><dd>{{ s.os }}</dd>
<dt>Hardware</dt><dd>{{ s.model }}</dd>
</dl>
</section>
</div>
{% endblock %}
+7
View File
@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<section class="card card-narrow">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</section>
{% endblock %}
+73
View File
@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% from "_macros.html" import badge, action, csrf with context %}
{% set title = "Kiosk" %}
{% block content %}
<div class="page-head">
<h1>Kioskbeheer</h1>
{{ badge(s.browser.status, s.browser.label) }}
</div>
<section class="actions-row">
{{ action(url_for('ui.kiosk_reload'), "Pagina herladen", "btn-primary", big=True) }}
{{ action(url_for('ui.kiosk_restart'), "Chromium herstarten", "btn-warning", confirm="Chromium wordt afgesloten en opnieuw gestart.", big=True) }}
</section>
<div class="grid grid-2">
<section class="card">
<h2>Instellingen</h2>
<form method="post" action="{{ url_for('ui.kiosk_settings') }}" class="form">
{{ csrf() }}
<label>Kiosk-URL
<input type="url" name="url" value="{{ conf.url }}" required pattern="https://.+" maxlength="2048" autocomplete="off" spellcheck="false">
<small>Alleen HTTPS. Een toegangstoken in de URL (zoals <code>?token=</code> van InfoFlow) wordt elders afgeschermd getoond.</small>
</label>
<label>Apparaatnaam
<input type="text" name="device_name" value="{{ conf.device_name }}" required maxlength="64">
</label>
<label>Automatisch verversen
<select name="refresh_interval">
{% set intervals = [(0, "Uit"), (60, "Elke minuut"), (300, "Elke 5 minuten"), (900, "Elk kwartier"), (1800, "Elk half uur"), (3600, "Elk uur"), (21600, "Elke 6 uur"), (86400, "Elke dag")] %}
{% set known = intervals|map(attribute=0)|list %}
{% for value, label in intervals %}<option value="{{ value }}" {% if conf.refresh_interval == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
{% if conf.refresh_interval not in known %}<option value="{{ conf.refresh_interval }}" selected>Elke {{ conf.refresh_interval }} seconden</option>{% endif %}
</select>
<small>Wordt overgeslagen zolang de URL niet bereikbaar is, zodat een offline werkende pagina blijft staan.</small>
</label>
<label>Browserprofiel
<select name="browser_profile">
<option value="persistent" {% if conf.browser_profile == 'persistent' %}selected{% endif %}>Bewaren (cookies en offline-cache blijven na herstart)</option>
<option value="ephemeral" {% if conf.browser_profile == 'ephemeral' %}selected{% endif %}>Alleen in werkgeheugen (schoon bij iedere start)</option>
</select>
</label>
<label class="check"><input type="checkbox" name="browser_restart" value="1" {% if conf.browser_restart %}checked{% endif %}> Chromium automatisch herstarten na een crash</label>
<button type="submit" class="btn btn-primary">Opslaan</button>
</form>
</section>
<section class="card">
<h2>Huidig scherm</h2>
<div class="screenshot" data-screenshot="{{ url_for('ui.kiosk_screenshot') }}">
<p class="muted" data-screenshot-msg>Klik op ‘Screenshot maken’ om te zien wat de kiosk nu toont.</p>
<img alt="Screenshot van het kioskscherm" hidden>
</div>
<button type="button" class="btn btn-secondary" data-screenshot-btn>Screenshot maken</button>
<dl class="kv kv-tight">
<dt>Ingestelde URL</dt><dd class="break">{{ masked_url }}</dd>
<dt>Getoonde pagina</dt><dd class="break">{{ agent.page_url or "–" }}</dd>
<dt>Laatste controle</dt><dd>{% if agent.checked_at %}<time data-epoch="{{ agent.checked_at }}"></time> — {% if agent.target_reachable %}bereikbaar{% else %}{{ agent.reason or "niet bereikbaar" }}{% endif %}{% else %}–{% endif %}</dd>
{% if agent.offline_since %}<dt>Offline sinds</dt><dd><time data-epoch="{{ agent.offline_since }}"></time></dd>{% endif %}
</dl>
</section>
</div>
<section class="card">
<h2>Laatste meldingen</h2>
{% if errors %}
<pre class="log log-short">{% for line in errors %}{{ line }}
{% endfor %}</pre>
{% else %}
<p class="muted">Geen waarschuwingen of fouten.</p>
{% endif %}
<p><a href="{{ url_for('ui.logs', source='kiosk') }}">Alle kiosklogs bekijken →</a></p>
</section>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% set title = "Inloggen" %}
{% block content %}
<section class="card card-narrow">
<h1>Inloggen</h1>
<p class="muted">{{ device_name }} · {{ hostname }}</p>
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
<form method="post" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label>Beheerderswachtwoord
<input type="password" name="password" required autofocus autocomplete="current-password" maxlength="128">
</label>
<button type="submit" class="btn btn-primary btn-block">Inloggen</button>
</form>
<p class="muted small">Wachtwoord vergeten? Log via SSH in en voer <code>sudo voxip-kiosk-ctl reset-password</code> uit.</p>
</section>
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% set title = "Logboek" %}
{% block content %}
<div class="page-head"><h1>Logboek</h1></div>
<section class="card">
<form method="get" class="form form-inline">
<label>Bron
<select name="source">
{% for key, item in sources.items() %}<option value="{{ key }}" {% if key == source %}selected{% endif %}>{{ item[0] }}</option>{% endfor %}
</select>
</label>
<label>Regels
<select name="lines">
{% for n in line_options %}<option value="{{ n }}" {% if n == lines %}selected{% endif %}>{{ n }}</option>{% endfor %}
</select>
</label>
<label>Niveau
<select name="priority">
<option value="" {% if not priority %}selected{% endif %}>Alles</option>
<option value="warning" {% if priority == 'warning' %}selected{% endif %}>Waarschuwingen en fouten</option>
<option value="err" {% if priority == 'err' %}selected{% endif %}>Alleen fouten</option>
</select>
</label>
<button type="submit" class="btn btn-primary">Tonen</button>
</form>
<pre class="log" data-scroll-bottom>{{ text }}</pre>
</section>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% set title = "Beheerinterface herstart" %}
{% block content %}
<section class="card card-narrow" data-redirect="{{ target }}" data-redirect-after="8">
<h1>Beheerinterface herstart</h1>
<p>De beheerinterface is straks bereikbaar op:</p>
<p><a href="{{ target }}"><code>{{ target }}</code></a></p>
<p class="muted">Je wordt over enkele seconden automatisch doorgestuurd. Bij HTTPS met een zelfondertekend
certificaat vraagt je browser eenmalig om bevestiging.</p>
</section>
{% endblock %}
+100
View File
@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% from "_macros.html" import badge, csrf with context %}
{% set title = "Netwerk" %}
{% block content %}
<div class="page-head"><h1>Netwerkbeheer</h1></div>
{% if pending %}
<section class="card card-pending" data-pending="{{ url_for('ui.api_network_pending') }}" data-remaining="{{ pending.remaining }}">
<h2>Netwerkwijziging bevestigen</h2>
<p>De nieuwe instellingen voor <strong>{{ 'ethernet' if pending.interface == 'eth0' else 'wifi' }}</strong>
{% if pending.method == 'static' %}(statisch adres <strong>{{ pending.address }}</strong>){% else %}(DHCP){% endif %} zijn actief.
Werkt de beheerinterface nog? Bevestig dan. Zonder bevestiging worden over
<strong data-countdown>{{ pending.remaining }}</strong> seconden automatisch de vorige instellingen teruggezet.</p>
{% if pending.method == 'static' %}
<p class="muted">Is het adres veranderd? Open dan <code>{{ admin_scheme }}://{{ pending.address.split('/')[0] }}:{{ admin_port }}/network</code>, log in en bevestig daar.</p>
{% endif %}
<div class="actions-row">
<form method="post" action="{{ url_for('ui.network_confirm') }}" class="inline-form">{{ csrf() }}<button class="btn btn-success btn-big">Bevestigen</button></form>
<form method="post" action="{{ url_for('ui.network_rollback') }}" class="inline-form">{{ csrf() }}<button class="btn btn-secondary btn-big">Nu terugdraaien</button></form>
</div>
</section>
{% endif %}
{% if not net.available %}
<div class="alert alert-error">NetworkManager (nmcli) is niet beschikbaar op dit systeem.</div>
{% endif %}
<div class="grid grid-2">
{% for iface in ['eth0', 'wlan0'] %}
{% set d = net.devices[iface] %}
<section class="card">
<h2>{{ 'Ethernet' if iface == 'eth0' else 'Wifi' }} <span class="muted small">({{ iface }})</span></h2>
<dl class="kv">
<dt>Status</dt><dd>{{ badge('ok' if d.connected else ('starting' if d.state == 'connecting' else 'stopped'), d.state_label) }}</dd>
{% if d.connection %}<dt>Verbinding</dt><dd>{{ d.connection }}</dd>{% endif %}
<dt>IP-adres</dt><dd>{{ d.addresses|join(', ') or '–' }}</dd>
<dt>Gateway</dt><dd>{{ d.gateway or '–' }}</dd>
<dt>DNS</dt><dd>{{ d.dns|join(', ') or '–' }}</dd>
{% if d.hwaddr %}<dt>MAC</dt><dd><code>{{ d.hwaddr }}</code></dd>{% endif %}
</dl>
</section>
{% endfor %}
</div>
<div class="grid grid-2">
<section class="card">
<h2>Ethernet instellen</h2>
{% set p = net.devices.eth0.profile %}
<form method="post" action="{{ url_for('ui.network_apply') }}" class="form" data-ipform
data-confirm="De netwerkinstellingen worden gewijzigd. Bevestig daarna binnen {{ rollback_seconds // 60 }} minuten, anders worden ze teruggezet." data-confirm-title="Ethernet wijzigen">
{{ csrf() }}
<input type="hidden" name="interface" value="eth0">
{% include "_ipv4_fields.html" %}
<button type="submit" class="btn btn-primary">Toepassen</button>
</form>
</section>
<section class="card">
<h2>Wifi instellen</h2>
{% set p = net.devices.wlan0.profile %}
<form method="post" action="{{ url_for('ui.network_apply') }}" class="form" data-ipform
data-confirm="De wifi-instellingen worden gewijzigd. Bevestig daarna binnen {{ rollback_seconds // 60 }} minuten, anders worden ze teruggezet." data-confirm-title="Wifi wijzigen">
{{ csrf() }}
<input type="hidden" name="interface" value="wlan0">
<label>Netwerknaam (SSID)
<span class="input-row">
<input type="text" name="ssid" value="{{ p.ssid }}" required maxlength="32" list="wifi-networks" autocomplete="off" spellcheck="false">
<button type="button" class="btn btn-secondary" data-wifi-scan="{{ url_for('ui.network_scan') }}">Scannen</button>
</span>
<datalist id="wifi-networks"></datalist>
</label>
<ul class="scan-results" data-scan-results hidden></ul>
<label>Wachtwoord
<input type="password" name="psk" minlength="8" maxlength="64" autocomplete="new-password" {% if p.exists %}placeholder="Leeg laten om het huidige wachtwoord te behouden"{% endif %}>
</label>
<label class="check"><input type="checkbox" name="open" value="1"> Open netwerk (zonder wachtwoord)</label>
<label class="check"><input type="checkbox" name="hidden" value="1" {% if p.hidden %}checked{% endif %}> Verborgen netwerk</label>
{% include "_ipv4_fields.html" %}
<button type="submit" class="btn btn-primary">Toepassen</button>
</form>
<h2 class="mt">Wifi-land</h2>
<form method="post" action="{{ url_for('ui.network_country') }}" class="form form-inline">
{{ csrf() }}
<label>Landcode
<input type="text" name="country" value="{{ net.wifi_country or 'NL' }}" required maxlength="2" pattern="[A-Za-z]{2}" class="input-short">
</label>
<button type="submit" class="btn btn-secondary">Instellen</button>
</form>
</section>
</div>
<section class="card">
<h2>Herstel bij een onbereikbare kiosk</h2>
<ul class="plain">
<li>Iedere wijziging wordt na {{ rollback_seconds // 60 }} minuten automatisch teruggedraaid als je hem niet bevestigt, ook als de stroom tussendoor uitvalt.</li>
<li>Noodherstel: zet een leeg bestand <code>voxip-network-reset</code> op de bootpartitie van de SD-kaart en start de Pi. Alle VoxIP-netwerkprofielen worden dan verwijderd (terug naar DHCP).</li>
</ul>
</section>
{% endblock %}
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% set title = "Wachtwoord wijzigen" %}
{% block content %}
<section class="card card-narrow">
<h1>Wachtwoord wijzigen</h1>
<form method="post" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label>Huidig wachtwoord
<input type="password" name="current_password" required autocomplete="current-password" maxlength="128">
</label>
<label>Nieuw wachtwoord
<input type="password" name="password" required minlength="10" maxlength="128" autocomplete="new-password">
</label>
<label>Herhaal nieuw wachtwoord
<input type="password" name="password_confirm" required minlength="10" maxlength="128" autocomplete="new-password">
</label>
<button type="submit" class="btn btn-primary btn-block">Wijzigen</button>
</form>
<p class="muted small">Na het wijzigen worden alle andere sessies uitgelogd.</p>
</section>
{% endblock %}
+24
View File
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% set title = "Eerste configuratie" %}
{% block content %}
<section class="card card-narrow">
<h1>Welkom bij VoxIP Kiosk Manager</h1>
<p>Stel eerst een uniek beheerderswachtwoord in. Je hebt daarvoor de <strong>setupcode</strong> nodig die op het
scherm van de kiosk staat (of via SSH: <code>sudo voxip-kiosk-ctl setup-code</code>).</p>
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
<form method="post" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label>Setupcode
<input type="text" name="setup_code" required autocomplete="off" autocapitalize="characters" spellcheck="false" placeholder="ABCD-EFGH" maxlength="12" class="input-code">
</label>
<label>Nieuw wachtwoord
<input type="password" name="password" required minlength="10" maxlength="128" autocomplete="new-password">
<small>Minimaal 10 tekens. Gebruik voor iedere kiosk een eigen wachtwoord.</small>
</label>
<label>Herhaal wachtwoord
<input type="password" name="password_confirm" required minlength="10" maxlength="128" autocomplete="new-password">
</label>
<button type="submit" class="btn btn-primary btn-block">Wachtwoord instellen</button>
</form>
</section>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
{% extends "base.html" %}
{% from "_macros.html" import action, csrf with context %}
{% set title = "Systeem" %}
{% block content %}
<div class="page-head"><h1>Systeembeheer</h1></div>
<section class="actions-row">
{{ action(url_for('ui.kiosk_restart'), "Chromium herstarten", "btn-warning", confirm="Chromium wordt afgesloten en opnieuw gestart.", big=True) }}
{{ action(url_for('ui.system_reboot'), "Raspberry Pi herstarten", "btn-danger", confirm="De Raspberry Pi herstart. Het scherm en de beheerinterface zijn ongeveer een minuut niet beschikbaar.", big=True) }}
{{ action(url_for('ui.system_poweroff'), "Veilig uitschakelen", "btn-dark", confirm="De Raspberry Pi wordt uitgeschakeld. Daarna kan hij alleen weer aan door de stroom los en weer vast te maken.", confirm_title="Raspberry Pi uitschakelen", big=True) }}
</section>
<div class="grid grid-2">
<section class="card">
<h2>Scherm</h2>
<form method="post" action="{{ url_for('ui.system_display') }}" class="form">
{{ csrf() }}
<label>Rotatie
<select name="rotation">
{% for deg, label in [(0, "0° (normaal)"), (90, "90° (rechtsom)"), (180, "180° (ondersteboven)"), (270, "270° (linksom)")] %}
<option value="{{ deg }}" {% if conf.rotation == deg %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Resolutie
<select name="resolution">
<option value="auto" {% if conf.resolution == 'auto' %}selected{% endif %}>Automatisch (voorkeur van het scherm)</option>
{% for mode in modes %}<option value="{{ mode }}" {% if conf.resolution == mode %}selected{% endif %}>{{ mode }}</option>{% endfor %}
{% if conf.resolution != 'auto' and conf.resolution not in modes %}<option value="{{ conf.resolution }}" selected>{{ conf.resolution }}</option>{% endif %}
</select>
<small>{% if output %}Aangesloten: {{ output.name }}{% if output.current %}, nu {{ output.current }}{% endif %}.{% else %}Schermgegevens verschijnen zodra de kiosk draait.{% endif %}</small>
</label>
<label>Zoomfactor
<input type="number" name="zoom" min="0.25" max="4" step="0.05" value="{{ conf.zoom }}">
<small>1 = normaal. Groter maakt alles groter (handig op grote tv's).</small>
</label>
<label class="check"><input type="checkbox" name="screen_always_on" value="1" {% if conf.screen_always_on %}checked{% endif %} data-toggle-target="#screen-off"> Scherm altijd aan (geen screensaver of energiebeheer)</label>
<label id="screen-off">Scherm uitschakelen na (minuten zonder invoer)
<input type="number" name="screen_off_minutes" min="1" max="240" value="{{ conf.screen_off_minutes }}">
</label>
<label class="check"><input type="checkbox" name="apply_now" value="1" checked> Direct toepassen (Chromium herstart)</label>
<button type="submit" class="btn btn-primary">Opslaan</button>
</form>
</section>
<section class="card">
<h2>Hostname</h2>
<form method="post" action="{{ url_for('ui.system_hostname') }}" class="form">
{{ csrf() }}
<label>Hostname
<input type="text" name="hostname" value="{{ hostname }}" required maxlength="63" pattern="[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?" autocomplete="off" spellcheck="false">
<small>De beheerinterface is daarna bereikbaar via <code>http://&lt;hostname&gt;.local:{{ conf.admin_port }}</code>. Geef iedere kiosk op hetzelfde netwerk een unieke naam.</small>
</label>
<button type="submit" class="btn btn-primary">Hostname wijzigen</button>
</form>
<h2 class="mt">Beheerinterface</h2>
<form method="post" action="{{ url_for('ui.system_admin') }}" class="form" data-confirm="Als je poort of HTTPS wijzigt, herstart de beheerinterface en word je doorgestuurd naar het nieuwe adres." data-confirm-title="Beheerinterface aanpassen">
{{ csrf() }}
<label>Poort
<input type="number" name="admin_port" min="1024" max="65535" value="{{ conf.admin_port }}">
</label>
<label class="check"><input type="checkbox" name="admin_https" value="1" {% if conf.admin_https %}checked{% endif %}> HTTPS gebruiken (zelfondertekend certificaat)</label>
<label class="check"><input type="checkbox" name="admin_allow_public" value="1" {% if conf.admin_allow_public %}checked{% endif %}> Toegang vanaf publieke IP-adressen toestaan <span class="muted">(afgeraden; gebruik liever VPN)</span></label>
<button type="submit" class="btn btn-secondary">Opslaan</button>
</form>
</section>
</div>
<section class="card">
<h2>Systeeminformatie</h2>
<dl class="kv">
<dt>Versie</dt><dd>VoxIP Kiosk OS {{ s.version }}</dd>
<dt>Besturingssysteem</dt><dd>{{ s.os }}</dd>
<dt>Hardware</dt><dd>{{ s.model }}</dd>
<dt>Uptime</dt><dd>{{ s.uptime }}</dd>
</dl>
<p><a href="{{ url_for('ui.logs', source='system') }}">Systeemlogs bekijken →</a></p>
</section>
{% endblock %}