{# Component library for the panel. Every component here is driven by an explicit context, so a template never reaches into a manager or a model. The state vocabulary is fixed and shared: "active", "idle", "failed" and "busy". Those four words are what the state rail, the badge and the notice all agree on, so an operator learns the language once and it holds on every screen. #} {% set _STATE_ALIASES = { "active": "active", "running": "active", "valid": "active", "enabled": "active", "failed": "failed", "error": "failed", "expired": "failed", "corrupt": "failed", "busy": "busy", "deploying": "busy", "renewing": "busy", "expiring": "busy", "building": "busy", "restoring": "busy" } %} {% macro state_class(state) -%} {{- _STATE_ALIASES.get(state | lower, "idle") -}} {%- endmacro %} {% macro badge(state, text=none) -%} {{ text or state }} {%- endmacro %} {# A resource row. Its state travels as a pill with a coloured dot - the v2 direction retired the 3px rail - so running your eye down a list still tells you the health of everything on it, in words as well as colour. meta is a list of (label, value) pairs rendered in mono. Keep it to the three or four facts an operator scans for, not everything the record holds. note is one further line, for what a tool said about this record verbatim: certbot's own validity line, or a backup's description. It is not a place for commentary. state_text is the state in words; when the caller has none, the pill carries the state's own name, because a dot on its own is unreadable to anyone who cannot see this particular pair of hues and invisible to a screen reader. Used with {% call %} so the caller supplies the action buttons: {% call m.row("example.com", "active", meta) %} {{ m.action_button("Restart", "/api/apps/example.com/restart") }} {% endcall %} #} {% macro row(name, state, meta=[], href=none, row_id=none, note=none, state_text=none) %}
{%- if href %}{{ name }}{% else %}{{ name }}{% endif -%} {{ badge(state, state_text or state) }} {% if meta %} {% for label, value in meta %}{{ label }} {{ value }}{% endfor %} {% endif %} {% if note %}{{ note }}{% endif %}
{{ caller() if caller else "" }}
{% endmacro %} {# Page header. Used with {% call %} so the caller supplies the toolbar: {% call m.page_head("Applications", "3 deployed") %} Deploy {% endcall %} #} {% macro page_head(title, subtitle=none) %}

{{ title }}

{% if subtitle %}

{{ subtitle }}

{% endif %}
{{ caller() if caller else "" }}
{% endmacro %} {# An empty screen is an invitation to act, not a shrug. It always names the one thing to do next, and gives the equivalent command, because this audience often prefers the terminal and should not have to guess the incantation. #} {% macro empty(title, body, action_label=none, action_href=none, command=none) %}

{{ title }}

{{ body }}

{% if action_label and action_href %} {{ action_label }} {% endif %} {% if command %}

Or run {{ command }}

{% endif %}
{% endmacro %} {# A message from nginx, systemd or certbot is never paraphrased. The fix goes above it in plain words; the tool's own output goes below it verbatim, in mono, because that is what the operator will search for. #} {% macro problem(fix, output=none) %} {% endmacro %} {# A capacity bar reads faster than a percentage, and both together read fastest. Thresholds live here so every meter in the panel agrees on what "nearly full" means. #} {# The fill is a class, not an inline width. The panel sets style-src 'self' with no unsafe-inline, because it runs as root and a style attribute is a place markup can be injected. The browser refused every style="width: N%" here, so the capacity bars rendered empty on every page: the one element on the machine strip whose whole job is to be read at a glance showed nothing at all. Rounding to the nearest five is what makes a class per value practical. It costs no accuracy that matters at 3.5rem wide, and the exact figure is in the number beside it either way. #} {% macro capacity(label, used, total, human_used="", human_total="") %} {%- set pct = (used / total * 100) if total else 0 -%} {%- set level = "critical" if pct >= 90 else ("warn" if pct >= 75 else "ok") -%} {%- set step = ((pct / 5) | round | int) * 5 -%} {{ label }} {{ pct | round | int }}% {% endmacro %} {% macro stat(label, value, unit=none) %} {{ label }} {{ value }}{% if unit %}{{ unit }}{% endif %} {% endmacro %} {# Destructive actions ask first, and the question names the exact resource and the exact consequence. "Are you sure?" tells an operator nothing. Pass question when the caller has already composed the exact wording. The swap is "delete", not "outerHTML": the endpoints answer in JSON, and swapping a JSON body into the list is how a delete used to leave a blob of braces where the row had been. htmx does not swap at all on a 4xx or 5xx, so a row only disappears when the deletion actually happened. #} {% macro danger_button(label, endpoint, resource, consequence, question=none, target=none) %} {# target defaults to the row this button sits in, which is where it sits everywhere except an application's own page: there is no .row there, so "closest .row" matched nothing, htmx raised targetError and never sent the request at all. The delete control on the detail screen was completely inert and said nothing about it. A caller outside a list passes its own target. #} {% endmacro %} {# Fires an action and swaps nothing, for the same reason: the API answers with JSON. What comes back is a job, and the job is reported by the machine strip, the activity screen and the notices, not by rewriting the row from a payload the template cannot read. done is what the operator is told once it worked, and it is written in the past tense of the button's own label, because the design direction commits to an action keeping its name through the whole flow: "Restart" produces "Restarted". Without it the panel ran the action and said nothing at all, which is how an operator ends up clicking Restart four times. #} {% macro action_button(label, endpoint, confirm=none, done=none) %} {% endmacro %} {# A two column sheet of facts: label on the left in the condensed label face, the machine's own value on the right in mono with tabular figures. Used on the detail screens, where the point is to be readable next to the output of the command that produced it. #} {% macro facts(entries) %} {% for label, value in entries %} {% endfor %}
{{ label }} {{ value }}
{% endmacro %} {# A value the server refused to send. It has to read as hidden, not as empty: a blank field says "nothing is set here" and invites someone to overwrite a working credential with an empty string. #} {% macro secret(value) %} {{ value }} {% endmacro %} {# Attaches the docked drawer to a log stream. The drawer itself is in base.html and persists across navigation, so this only tells it what to follow. The source and URL travel as data attributes, picked up by a delegated listener. An inline onclick carrying interpolated server data is how the previous panel ended up with a hundred and three places where a domain name could break out of an attribute. #} {% macro follow_button(label, source, url) %} {% endmacro %}