VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
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:
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 %}
|
||||
@@ -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 & 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 %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="card card-narrow">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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://<hostname>.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 %}
|
||||
Reference in new issue
Block a user