{% extends "ui/_layout.html" %} {% block title %}Images - bty-web{% endblock %} {% block subnav %} {% set right_html %}{{ unified|length }} entr{{ "y" if unified|length == 1 else "ies" }} | image-root {{ image_root }}{% endset %} {% with sections=[ {"key": "list", "label": "List", "icon": "list-ul", "href": "/ui/images"}, {"key": "fetch", "label": "Fetch catalog", "icon": "cloud-download","href": "/ui/images?section=fetch"}, {"key": "upload-catalog", "label": "Upload catalog", "icon": "file-earmark-text", "href": "/ui/images?section=upload-catalog"}, {"key": "upload-image", "label": "Upload image", "icon": "upload", "href": "/ui/images?section=upload-image"}, {"key": "upload-image-from-url", "label": "Upload image (from URL)", "icon": "cloud-plus", "href": "/ui/images?section=upload-image-from-url"}, ] , active=section, right_html=right_html %} {% include "ui/_subnav.html" %} {% endwith %} {% endblock %} {% block intro %} {% from "ui/_intro_box.html" import render as intro_box %} {% call intro_box() %} Pre-built system images bty flashes onto target disks. Populate via Fetch catalog to pull the bty release catalog, paste an image / oras URL, upload a catalog file, or drop files directly into {{ image_root }}. Entries are content- addressed: bty refuses to flash an entry whose sha hasn't been computed yet (the background hash worker picks new files up automatically). {% endcall %} {% endblock %} {% block content %} {% if section == "fetch" %} {# The common-case default: pull the bty project's released catalog.toml in one click. Operators who just want "give me something to flash" land here, click once, get a populated catalog. Custom URLs / uploads are one sub-nav click away. #}
Fetch from project release

Pulls releases/latest/download/catalog.toml from {{ release_repo }} and imports its entries into the catalog.

{% elif section == "upload-image-from-url" %}
Upload image (from URL)
{# type="text" rather than type="url": some browsers reject non-http schemes (oras://) under HTML5 URL validation. Server-side Pydantic validation still runs (regex covers http/https/oras schemes). #}
For http(s):// URLs, the first flash trusts the upstream bytes and back-fills disk_image_sha with what it observed. For oras:// refs the server resolves the OCI manifest at add time and uses the layer's content-addressed digest.
{% elif section == "upload-catalog" %}
Upload catalog
Parses before clobbering the existing catalog (a bad upload bounces back with the parse error). Replaces {{ manifest_path }} and reloads the download manager in-process.
{% elif section == "upload-image" %}
Upload image file
{# Streams the file via XHR PUT to /images/ -- the existing API endpoint that takes ``application/octet- stream``. JS handles progress + auth (cookie, same- origin); on success we redirect back to the list so the new file shows up. The HashManager picks up the upload via PUT /images/'s post-write hook (auto-enqueue), so a SHA appears in seconds. #}
0%
Streams directly into the image root via PUT /images/<name>. SHA-256 is auto-computed in the background after upload completes.
{% else %} {# section == "list" (default): the OBSERVABLE state of the catalog plus the live downloads + hashes panes. This is what an operator wants to see when they click "Images" in the top nav. Adding things is one sub-nav click away (above). #} {% if not unified %}
No images yet. The fastest way to populate: click Fetch default to pull the project's release catalog. Or drop a .qcow2 / .img / .img.zst / .img.xz / .img.gz / .img.bz2 file under {{ image_root }} and refresh.
{% else %}
Unified catalog (dir scan + manifest, SHA-keyed)
{# Observed content sha (``disk_image_sha`` on the catalog row). Machines bind via ``bty_image_ref`` -- shown in the machine- detail picker, not here. #} {% for u in unified %} {% endfor %}
Name(s)Content SHA Format Sources Cached Action
{% for n in u.names %} {{ n }}{% if not loop.last %}, {% endif %} {% endfor %} {% if u.sha256 %} {{ u.sha256[:12] }}... {% else %} unhashed {% endif %} {{ u.format or "?" }} {% for s in u.sources %} {% if s.kind == "local" %} {{ s.location }} {% elif s.kind == "url" %} {{ s.location }} {% else %} {{ s.location }} {% endif %} {% if not loop.last %}
{% endif %} {% endfor %}
{% if u.cached %} cached {% else %} available {% endif %} {# Dispatch by source shape, not just sha presence: - Has a ``local`` source and no sha -> the file is on disk and just needs hashing. Hash button. - No local source -> bytes need fetching. Fetch button. Works for un-sha'd URL / oras entries (the DownloadManager downloads + computes sha + back-fills catalog_entries.disk_image_sha) AND for sha-pinned entries not yet in cache. Previously every un-sha'd entry rendered the Hash button, which 404'd on URL-only rows because HashManager requires a local file. #} {% set has_local = u.sources|selectattr('kind','equalto','local')|list|length > 0 %} {% if not u.sha256 and has_local %} {% elif not u.cached %} {% endif %} {# Evict cached bytes. Available for any entry whose bytes are in the cache dir (``$BTY_STATE_DIR/cache/``). Backed by ``DELETE /catalog/cache/{name}`` which unlinks the cached file but preserves the catalog entry's metadata -- the next Fetch re-downloads. Useful for forcing a re-pull when a rolling tag has rotated upstream. Suppressed for purely-local rows (``local`` source only) since deleting those would unlink the operator's source file rather than just a cache copy. #} {% set has_remote = u.sources|selectattr('kind','in',['manifest','url'])|list|length > 0 %} {% if u.cached and has_remote %} {% endif %} {# Delete the catalog entry. Backed by ``DELETE /catalog/entries?src=`` which wipes the catalog_entries row. The button shows for every entry that has at least one ``manifest`` or ``url``-kind source (i.e. everything except local-only dir-scan rows, where the right delete action is to remove the underlying file). #} {% if has_remote %} {% set entry_src = u.sources|selectattr('kind','in',['manifest','url'])|map(attribute='location')|first %} {% endif %} {% if not has_remote and not u.cached and not (not u.sha256 and has_local) %} - {% endif %}
Downloads refreshing...
Name Status Progress Bytes Action
No downloads yet.
Hashes background SHA-256 of dir-scan files; default 1 at a time so small hardware (Pi / NUC) stays responsive
Name Status Progress Bytes Action
No hashes yet.
{% endif %} {% endif %} {# Image-relevant event slice (uploads, hashes, catalog adds / deletes). The /ui/events drill-down filters to subject_kind= image; catalog rows are picked up via the broader filter at /ui/events itself. Shown on every section so an operator doing an upload sees the recent activity context. #} {% with events=image_events, title="Recent image-catalog activity", link_to_full="/ui/events?subject_kind=image" %} {% include "ui/_events_card.html" %} {% endwith %} {% endblock %}