{% extends "ui/_layout.html" %}
{% block title %}Images - bty-web{% endblock %}
{% block subnav %}
{% with sections=[
{"key": "list", "label": "List", "icon": "list-ul", "href": "/ui/images"},
{"key": "downloads", "label": "Downloads", "icon": "arrow-down-circle", "href": "/ui/images?section=downloads"},
{"key": "hashes", "label": "Hashes", "icon": "hash", "href": "/ui/images?section=hashes"},
] , active=section %}
{% 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, content-
addressed by sha256. Sub-nav:
List is the merged catalog (dir scan + catalog
entries): its header carries Fetch latest catalog (pull
the bty release catalog) and Upload catalog (a catalog.toml);
Downloads is where you add a single image
(upload a file or add by URL) and watch the live fetch jobs;
Hashes shows the background sha worker. Files
dropped into {{ image_root }} are picked up and
hashed automatically.
{% endcall %}
{% endblock %}
{% block content %}
{% if section == "downloads" %}
{# Downloads: the two add-image controls (add by URL, upload a local
file) live inline in the table header, split by a vertical rule; the
live download-jobs table is below. #}
{# ``py-1`` + ``flex-nowrap`` keep this a single tight row: the two
add-image forms used to wrap to a second line (doubling the
header height) on a narrow window. The inputs shrink (min-width:0)
instead, and the button labels stay on one line (text-nowrap). #}
Downloads
refreshing...
{# type="text" not type="url": browsers reject oras:// under
HTML5 URL validation; server-side Pydantic still validates. #}
{# Streams via XHR PUT /images/; the HashManager
auto-enqueues a SHA on the post-write hook. #}
{# Upload progress (hidden until an upload runs). #}
0%
Name
Status
Progress
Bytes
Action
No downloads yet.
{% elif section == "hashes" %}
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.
{% else %}
{# ``py-1`` + ``flex-nowrap``: keep the catalog actions a single
tight row instead of wrapping to a second line on a narrow
window. The upload input shrinks; button labels stay intact. #}
Images
{# Two catalog actions side by side, separated by a vertical
rule: upload a local catalog.toml, or fetch the bty release
catalog.toml in one click (both were dropped sub-nav pages). #}
{# Parsed before it replaces the current catalog
({{ manifest_path }}); a bad file bounces back. #}
Name(s)
{# Observed content sha (``disk_image_sha`` on
the catalog row). Machines bind via
``bty_image_ref`` -- shown in the machine-
detail picker, not here. #}
Content SHA
Format
Sources
Cached
Action
{% for u in unified %}
{% for n in u.names %}
{{ n }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% 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 %}
{% endfor %}
{% if not unified %}
No images yet. Use Fetch latest or
Upload above, or drop a
.qcow2 / .img /
.img.zst / .img.xz /
.img.gz / .img.bz2 file into
{{ image_root }}.
{% 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 %}