{% extends "ui/_layout.html" %} {% from "ui/_table_macros.html" import sort_header, pagination_footer with context %} {% block title %}Machines - bty-web{% endblock %} {% block subnav %} {# In-page jump links to the two tables on this page, separated by a vertical rule (same style as the Settings section nav). #} {% endblock %} {% block intro %} {% from "ui/_intro_box.html" import render as intro_box %} {% call intro_box() %} Machines auto-appear on first remote contact (PXE or manual tui --server .. --mac ..); to stage one before it boots, use the Add field in the table header (image binding + boot mode are set per-machine on the detail page). {% endcall %} {% endblock %} {% block content %} {# Post-delete flash banner. A non-trivial action (delete a machine record) deserves operator feedback whether or not the row actually existed. ``?deleted=`` after a real removal; ``?missing=`` when the row was already gone (stale tab, re-clicked delete). Auto-dismiss after 5 seconds via the bootstrap data-bs-dismiss + setTimeout below. #} {% if flash_deleted %} {% elif flash_missing %} {% endif %} {% if flash_deleted or flash_missing %} {% endif %} {# The live table of all machines, with a minimal inline add-by-MAC field in the header. Machines also auto-appear on first PXE contact; the inline Add just stages a row early. #}
{# ``py-1`` + ``flex-nowrap`` keep the header a single tight row around the inline add-MAC form: default padding plus a wrap to a second line made the bar much taller than a table row. The input shrinks rather than wrapping on a narrow window. #}
Machines {# Minimal add-by-MAC: MAC only. boot_mode defaults to the safe "ipxe-exit" (never a flash policy from here); hostname, image binding + policy are set on the detail page. The upsert handler creates the row with discovered_at=NULL; it flips to first-seen on the box's first PXE contact. #}
{# Free-text search across MAC / hostname / image ref / last-seen IP. Submits via GET so the result URL is bookmarkable. Carries the active filter + sort + per_page through hidden inputs so a search inside a filtered/sorted view preserves the operator's view. #}
{% if active_filter %}{% endif %} {% if sort.column != 'mac' %}{% endif %} {% if sort.direction != 'asc' %}{% endif %} {% if page.per_page != per_page_choices[1] %}{% endif %} {% if q %} Clear Filtered by “{{ q }} {% endif %}
{# Pin the narrow columns to width:1% + nowrap (as the other tables do) so they stay tight and only MAC / Image / Hostname absorb the slack -- otherwise the header columns stretch evenly and look oversized. #} {{ sort_header('MAC', 'mac', sort, preserved, 'style="white-space: nowrap;"') }} {{ sort_header('Image', 'bty_image_ref', sort, preserved) }} {{ sort_header('Boot', 'boot_mode', sort, preserved, 'style="width: 1%; white-space: nowrap;"') }} {{ sort_header('Hostname', 'hostname', sort, preserved) }} {{ sort_header('Last seen', 'last_seen_at', sort, preserved, 'style="width: 1%; white-space: nowrap;"') }} {{ sort_header('Last flashed', 'last_flashed_at', sort, preserved, 'style="width: 1%; white-space: nowrap;"') }} {# The tbody subscribes to the SSE stream so it auto-updates when the operator (or auto-discovery) mutates machines. The browser carries the bty-token cookie automatically; that is what authenticates the EventSource connection. ``sse-target`` opts in to the layout-level flash animation. When a server-side filter is active (``?filter=...``) the SSE wiring is dropped: the next ``machines-update`` would replace the filtered tbody with the full un-filtered list, which is operator-confusing. The page becomes static under filter; reload to refresh. #} {% include "ui/_machines_tbody.html" %}
{{ pagination_footer(page, preserved, label='machines') }}
{# Recent machine activity (discoveries, flashes, inventory posts). The "Activity" sub-nav anchor jumps here; always rendered (empty-state row when there's nothing yet). #} {% with events=machine_events, title="Recent Events", link_to_full="/ui/events?subject_kind=machine", card_id="machines-activity" %} {% include "ui/_events_card.html" %} {% endwith %} {% endblock %}