{% 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
{% elif section == "upload-image-from-url" %}
Upload image (from URL)
{% elif section == "upload-catalog" %}
Upload catalog
{% 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)
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 %}
Downloadsrefreshing...
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 %}