{# The panel shell. Three things are present on every page: the machine strip, so you always know which box you are about to change; the navigation, with live counts; and the log drawer, docked at the bottom rather than hidden behind a click per resource. #} {% macro nav_item(href, label, key, count) -%} {{ label }} {%- if count is not none %}{{ count }}{% endif %} {%- endmacro %} {% block title %}{{ machine.hostname }}{% endblock %} - WASM {# Everything is served from this host. The panel works on a machine with no route to the internet, which is rather the point of self-hosting. #} {# Two scripts, and no Alpine. The panel serves itself under script-src 'self' with no unsafe-eval, because it runs systemd as root and an injected script here is a root shell. Alpine turns every x-data, @click and x-text into a function built from a string, which that policy forbids outright: the browser refused all of them, the log drawer never initialised, and the mobile navigation could not be opened. It was not a subtle failure - it was the whole framework never running - and no test could see it, because the markup was correct and the server was correct. The two islands it was there for are a hundred lines of panel.js now. Loosening the policy for a dependency that costs 47 KB and does not work is not a trade this product makes. #} {# Every mutation htmx sends carries the session's CSRF token. The header name comes from the server rather than being typed here: the shell used to send "X-CSRF-Token" while wasm.web.auth reads CSRF_HEADER_NAME, which is "X-WASM-CSRF", so every restart, every delete and the sign-out button came back 403 from a browser. Two spellings of one name in two files cannot be kept in agreement by care alone. #} {# The navigation's open state lives on the body, not on .shell, because the control that opens it has to sit outside the sidebar: a toggle inside the panel it reveals goes off-canvas with it, which is why the panel shipped with a sidebar that could be styled open and reached by nothing. #} {# Outside the machine strip on purpose: that fragment replaces itself every five seconds, and a control that is destroyed under the operator's finger takes their keyboard focus with it. #} {# The signature element: one dense instrument line for this machine. The fragment carries its own element so htmx can swap it whole. #} {% include "fragments/machine.html" %}
{# tabindex makes the skip link's destination focusable; without it the link moves the viewport and leaves the keyboard where it was. #}
{% block content %}{% endblock %}
{# The docked log drawer. It costs vertical space, and that is the bet: the moment logs matter most is mid-deploy, and hiding them behind a click per resource is what makes a web panel feel worse than an SSH session. #}
{# The bar is a plain element that happens to be clickable, and the toggle is a real button. It used to be the other way round: the bar carried role="button" and wrapped two more buttons, which is invalid nesting, and its keyboard handling was hand-rolled. A native button is focusable, operable with Enter and Space, and announced correctly without any of that. #}
Log {# The text is server-rendered and then kept in step by panel.js, so the bar reads correctly even in the instant before the script runs and on a machine where it never does. #} nothing attached