Move reload/restart/shutdown into an Acties menu in the top bar
CI / test-bookworm (push) Failing after 4s
CI / test-latest (push) Successful in 31s

The actions are now on every page in the same place instead of a button
row that differed per page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rothmanandClaude Opus 5.5 committed 2026-11-04 22:49:24 +01:00
1 parent 7e07428a81
commit efc581e0ae
8 files changed
+51 -21

No files matched your search

+16 -1
View File
@@ -42,6 +42,16 @@ h2 { font-size: 1.1rem; margin: 0 0 .9rem; }
.mainnav a:hover, .mainnav button:hover { background: var(--navy-2); color: #fff; } .mainnav a:hover, .mainnav button:hover { background: var(--navy-2); color: #fff; }
.mainnav a[aria-current="page"] { background: var(--navy-2); color: #fff; } .mainnav a[aria-current="page"] { background: var(--navy-2); color: #fff; }
.nav-logout { margin: 0; } .nav-logout { margin: 0; }
.nav-menu { position: relative; }
.nav-menu summary { list-style: none; color: #d6e2f3; padding: .45rem .7rem; border-radius: 6px; font-size: .95rem; cursor: pointer; user-select: none; }
.nav-menu summary::-webkit-details-marker { display: none; }
.nav-menu summary:hover, .nav-menu[open] summary { background: var(--navy-2); color: #fff; }
.nav-menu-list { position: absolute; right: 0; top: calc(100% + 6px); min-width: 230px; padding: .3rem 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 8px 24px rgba(11, 37, 69, .15); z-index: 20; }
.nav-menu-list form { margin: 0; }
.mainnav .nav-menu-list button { display: block; width: 100%; text-align: left; padding: .55rem .9rem; border-radius: 0; color: var(--text); }
.mainnav .nav-menu-list button:hover { background: #f1f4f8; color: var(--text); }
.mainnav .nav-menu-list button.danger { color: #b03030; }
.nav-menu-list hr { border: 0; border-top: 1px solid var(--border); margin: .3rem 0; }
.nav-toggle { display: none; background: none; border: 1px solid rgba(255, 255, 255, .3); border-radius: 8px; width: 44px; height: 40px; cursor: pointer; } .nav-toggle { display: none; background: none; border: 1px solid rgba(255, 255, 255, .3); border-radius: 8px; width: 44px; height: 40px; cursor: pointer; }
.nav-toggle .bars, .nav-toggle .bars::before, .nav-toggle .bars::after { display: block; width: 20px; height: 2px; background: #fff; margin: 0 auto; position: relative; content: ""; } .nav-toggle .bars, .nav-toggle .bars::before, .nav-toggle .bars::after { display: block; width: 20px; height: 2px; background: #fff; margin: 0 auto; position: relative; content: ""; }
.nav-toggle .bars::before { position: absolute; top: -6px; } .nav-toggle .bars::before { position: absolute; top: -6px; }
@@ -51,7 +61,12 @@ h2 { font-size: 1.1rem; margin: 0 0 .9rem; }
.nav-toggle { display: block; } .nav-toggle { display: block; }
.mainnav { display: none; width: 100%; flex-direction: column; align-items: stretch; padding-bottom: .75rem; } .mainnav { display: none; width: 100%; flex-direction: column; align-items: stretch; padding-bottom: .75rem; }
.mainnav.open { display: flex; } .mainnav.open { display: flex; }
.mainnav a, .mainnav button { padding: .75rem; text-align: left; width: 100%; } .mainnav a, .mainnav button, .nav-menu summary { padding: .75rem; text-align: left; width: 100%; }
.nav-menu-list { position: static; min-width: 0; margin: .25rem 0 0 .75rem; background: none; border: 0; border-left: 1px solid rgba(255, 255, 255, .2); border-radius: 0; box-shadow: none; }
.mainnav .nav-menu-list button { color: #d6e2f3; }
.mainnav .nav-menu-list button:hover { background: var(--navy-2); color: #fff; }
.mainnav .nav-menu-list button.danger { color: #ffb4b4; }
.nav-menu-list hr { border-top-color: rgba(255, 255, 255, .2); }
} }
/* --- layout ------------------------------------------------------------------ */ /* --- layout ------------------------------------------------------------------ */
+11
View File
@@ -16,6 +16,17 @@
}); });
} }
// Actions menu: close after choosing an item, on a click outside it or on Escape
const menu = $("[data-nav-menu]");
if (menu) {
document.addEventListener("click", (event) => {
if (menu.open && (!menu.contains(event.target) || event.target.closest(".nav-menu-list button"))) menu.open = false;
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && menu.open) menu.open = false;
});
}
// Confirmation dialog for dangerous actions (forms with data-confirm) // Confirmation dialog for dangerous actions (forms with data-confirm)
const dialog = $("#confirm-dialog"); const dialog = $("#confirm-dialog");
$$("form[data-confirm]").forEach((form) => { $$("form[data-confirm]").forEach((form) => {
+3 -3
View File
@@ -2,10 +2,10 @@
<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> <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 %} {%- endmacro %}
{% macro action(url, label, cls="btn-primary", confirm=None, confirm_title=None, big=False) -%} {% macro menu_action(url, label, confirm=None, confirm_title=None, danger=False) -%}
<form method="post" action="{{ url }}" class="inline-form" {% if confirm %}data-confirm="{{ confirm }}" data-confirm-title="{{ confirm_title or label }}"{% endif %}> <form method="post" action="{{ url }}" {% if confirm %}data-confirm="{{ confirm }}" data-confirm-title="{{ confirm_title or label }}"{% endif %}>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn {{ cls }}{% if big %} btn-big{% endif %}">{{ label }}</button> <button type="submit"{% if danger %} class="danger"{% endif %}>{{ label }}</button>
</form> </form>
{%- endmacro %} {%- endmacro %}
+11
View File
@@ -1,3 +1,4 @@
{% from "_macros.html" import menu_action with context %}
<!doctype html> <!doctype html>
<html lang="nl"> <html lang="nl">
<head> <head>
@@ -27,6 +28,16 @@
<a href="{{ url_for('ui.network') }}" {% if active == 'network' %}aria-current="page"{% endif %}>Netwerk</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.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> <a href="{{ url_for('ui.change_password') }}" {% if active == 'account' %}aria-current="page"{% endif %}>Wachtwoord</a>
<details class="nav-menu" data-nav-menu>
<summary>Acties <span aria-hidden="true">▾</span></summary>
<div class="nav-menu-list">
{{ menu_action(url_for('ui.kiosk_reload'), "Pagina herladen") }}
{{ menu_action(url_for('ui.kiosk_restart'), "Chromium herstarten", confirm="Chromium wordt afgesloten en opnieuw gestart. Het scherm is enkele seconden zwart.") }}
<hr>
{{ menu_action(url_for('ui.system_reboot'), "Raspberry Pi herstarten", confirm="De Raspberry Pi herstart. Het scherm en de beheerinterface zijn ongeveer een minuut niet beschikbaar.", danger=True) }}
{{ menu_action(url_for('ui.system_poweroff'), "Veilig uitschakelen", 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", danger=True) }}
</div>
</details>
<form method="post" action="{{ url_for('ui.logout') }}" class="nav-logout"> <form method="post" action="{{ url_for('ui.logout') }}" class="nav-logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit">Uitloggen</button> <button type="submit">Uitloggen</button>
+1 -6
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "_macros.html" import badge, action, meter with context %} {% from "_macros.html" import badge, meter with context %}
{% set title = "Dashboard" %} {% set title = "Dashboard" %}
{% block content %} {% block content %}
<div class="page-head"> <div class="page-head">
@@ -7,11 +7,6 @@
<p class="muted">Ververst automatisch elke 5 seconden.</p> <p class="muted">Ververst automatisch elke 5 seconden.</p>
</div> </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-secondary", 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') }}"> <div class="grid" data-live-status="{{ url_for('ui.api_status') }}">
<section class="card"> <section class="card">
+1 -5
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "_macros.html" import badge, action, csrf with context %} {% from "_macros.html" import badge, csrf with context %}
{% set title = "Kiosk" %} {% set title = "Kiosk" %}
{% block content %} {% block content %}
<div class="page-head"> <div class="page-head">
@@ -7,10 +7,6 @@
{{ badge(s.browser.status, s.browser.label) }} {{ badge(s.browser.status, s.browser.label) }}
</div> </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-secondary", confirm="Chromium wordt afgesloten en opnieuw gestart.", big=True) }}
</section>
<div class="grid grid-2"> <div class="grid grid-2">
<section class="card"> <section class="card">
+1 -6
View File
@@ -1,14 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "_macros.html" import action, csrf with context %} {% from "_macros.html" import csrf with context %}
{% set title = "Systeem" %} {% set title = "Systeem" %}
{% block content %} {% block content %}
<div class="page-head"><h1>Systeembeheer</h1></div> <div class="page-head"><h1>Systeembeheer</h1></div>
<section class="actions-row">
{{ action(url_for('ui.kiosk_restart'), "Chromium herstarten", "btn-secondary", 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-secondary", 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"> <div class="grid grid-2">
<section class="card"> <section class="card">
+7
View File
@@ -298,3 +298,10 @@ def test_admin_port_change_restarts_manager(logged_in, fake_helper, paths):
assert resp.status_code == 200 and ":8443/" in resp.get_data(as_text=True) assert resp.status_code == 200 and ":8443/" in resp.get_data(as_text=True)
assert fake_helper.calls[-1][0] == "restart-manager" assert fake_helper.calls[-1][0] == "restart-manager"
assert load_config(paths.config_file)["admin_port"] == 8443 assert load_config(paths.config_file)["admin_port"] == 8443
@pytest.mark.parametrize("path", ["/", "/kiosk", "/system", "/network", "/logs", "/account/password"])
def test_actions_menu_on_every_page(logged_in, path):
html = logged_in.get(path).get_data(as_text=True)
for action in ("/kiosk/reload", "/kiosk/restart", "/system/reboot", "/system/poweroff"):
assert f'action="{action}"' in html