{% extends "ui/_layout.html" %} {% block title %}Settings - bty-web{% endblock %} {% block subnav %} {# Settings is the one page with real sub-nav: in-page jump links to each card, separated by vertical rules. #} {% endblock %} {% block intro %} {% from "ui/_intro_box.html" import render as intro_box %} {% call intro_box() %} Where every bty magic value comes from. Most are read-only (set via environment variable or derived from the state directory); the Netboot release and Catalog cards are editable and persist in the database. The DHCP / Network boot cheatsheet below is the router-side config to point PXE / HTTP-Boot clients at this host. Operator authentication lives on the Account page. {% endcall %} {% endblock %} {% block content %} {# Both editable cards share the same POST handler (/ui/settings/upstream) but render as visually-distinct cards so the operator's mental model (netboot artifacts vs image catalog) maps onto the UI. The form element lives outside the row and is referenced by ``form= "upstream-form"`` on every input + button, which is the HTML5 way to associate fields with a non-ancestor form. Submitting from EITHER card saves all three fields in one round-trip. #}
{# Row 1: Catalog spans full width. The card carries an explanatory callout about the dual-flavour nosi catalog model plus the single URL input, so giving it full width keeps the call-out readable on wide screens. #}
Catalog

The single URL that the Fetch catalog button on the Images page GETs. Bytes at that URL must parse as a catalog.toml; the rows are imported into the local catalog table. To point at a fork or a private catalog server, just edit the URL.

The default URL pins to the nosi release this bty version was built against, so two operators on the same bty version see byte-identical catalog content; upgrading bty is what rolls the catalog forward. Three options if you want something else:

  • Pin to a newer week. Browse safl/nosi releases, pick a tag (e.g. 2026.W26), paste /releases/download/<tag>/catalog.toml.
  • Always-newest (rolls under your feet). Use /releases/latest/download/catalog.toml; each fetch is internally W-tag-pinned but the URL roll-forwards with each new nosi release.
  • Rolling oras :latest refs. Use /releases/latest/download/catalog-latest.toml (or /releases/download/<tag>/catalog-latest.toml); image refs are :latest so bty flashes whatever ghcr currently serves, every flash.
Effective: {{ upstream.catalog_url }}{% if not upstream.catalog_url_override %} (default; leave blank to keep){% endif %}.
Clear to revert.
{# /col catalog-source #}
{# /row catalog #} {# Row 2: Netboot release + Backup schedule side-by-side. #}
Netboot release

The GitHub release that the Fetch artifacts button on the Netboot page pulls vmlinuz, initrd, and squashfs from. Repo + tag are independent so an operator can pin netboot to a known-good release while the catalog rolls forward.

GitHub owner/repo. Effective: {{ upstream.netboot_repo }}{% if not upstream.netboot_repo_override %} (default; leave blank to keep){% endif %}.
latest or a tag like v0.23.0. Effective: {{ upstream.netboot_tag }}{% if not upstream.netboot_tag_override %} (default; leave blank to keep){% endif %}.
Clear a field to revert.
{# /col netboot-release #}
{# Editable: scheduled backup knobs. ``enabled`` + ``cadence`` + ``retention`` persist in the settings table; the scheduler loop reads them on every tick so a change here propagates within one tick (60s) without restart. The on-disk destination is read-only (env-overridable via BTY_BACKUP_DIR). #}
Backup schedule
{# Always-relevant block: retention applies on every successful backup (manual or scheduled), and destination + last-run are useful regardless of whether the scheduler is enabled. The schedule (enable + cadence) is the optional layer below the
-- it only matters if the operator wants bty to fire backups on its own. #}
Keep the N most recent backups under {{ backups_root }}. Older directories are deleted after every successful run -- manual or scheduled. Set to 1 to keep only the latest.
Destination: {{ backups_root }} {% if backup_last_run_at %}
Last scheduled run: {{ backup_last_run_at }} {% else %}
Last scheduled run: never {% endif %}

Off: bty never runs a backup on its own; operator triggers each one via "Back up now" on Backups. On: the scheduler enqueues a backup whenever the cadence is due.
{% for c in backup_cadences %}
{% endfor %}
{# /col backup-schedule #}
{# /row netboot + backup #} {# Four-up row of the config groups (Identity / Storage / Network / Background workers). Each card uses a compact list-group layout instead of the wide 4-column table the single-column form had: at col-lg-3 each card is too narrow for a table-with-source-and- env column set, so the source badge sits inline with the value and the env-var name renders as a small muted line below. On medium screens cards stack 2-up; on phones 1-up. rows that map to a Config field carry an inline edit form (POST -> /ui/settings/config/edit -> save_value to the primary bty.toml). Env-overridden rows render the input as disabled with a hint -- the TOML write wouldn't take effect until the env is unset. Pure-display rows (e.g. "bty version") keep the read-only shape. #}
{% for group in config_groups %}
{{ group.title }}
    {% for r in group.rows %}
  • {{ r.label }} {% if r.source == "env" %} env {% elif r.source == "toml" %} toml {% else %} default {% endif %}
    {% if r.env %}
    {{ r.env }}
    {% endif %} {% if r.editable %}
    {% elif r.source == "env" %} {# Env-overridden: show the current value as a disabled input + a hint that the env wins. #} {% else %} {# Pure-display row (e.g. bty version). #}
    {{ r.value }}
    {% endif %}
  • {% endfor %}
{% endfor %}
{# /row config_groups #} {# Source-of-truth banner: when the operator opens Settings and nothing is configured at the bty.toml layer (or none of the candidate paths are writable), surface that clearly -- editing below either no-ops or has nowhere to land. #} {% if not config_primary_toml %}
No writable bty.toml in the config search path. Settings edits below have nowhere to persist. Set $BTY_CONFIG_FILE to a writable path or create <state_dir>/bty.toml.
{% endif %} {# Router-side cheatsheet (moved here from the Netboot page). The bits the operator pastes into UniFi / pfSense / dnsmasq DHCP config to point PXE clients at this host. Reference-only; no editable fields -- bty doesn't run DHCP, the LAN router does. #}
DHCP / Network boot

Targets can net-boot two ways: PXE (legacy + UEFI, iPXE fetched over TFTP) and UEFI HTTP Boot (iPXE fetched over HTTP, no TFTP). bty serves the boot artifacts and per-MAC iPXE scripts over HTTP; TFTP is served by the tftp sidecar (container deploy) or a co-located dnsmasq (host install). The DHCP side stays with your LAN's router (or DHCP server); bty deliberately doesn't try to replace it. Point your router at this host with the settings below.

{% if missing_netboot_artifacts %}
Netboot environment incomplete. Missing under {{ boot_root }}: {% for name in missing_netboot_artifacts %} {{ name }}{% if not loop.last %}, {% endif %} {% endfor %}. PXE clients will chain into iPXE but get 404 on the kernel fetch until these files are present.
Fetch artifacts
{% endif %}

This host

{% for iface in interfaces %} {% else %} {% endfor %}
Interface State IPv4
{{ iface.name }} {{ iface.operstate }} {% if iface.ipv4 %} {{ iface.ipv4 }}/{{ iface.prefix }} {% else %} (no address) {% endif %}
(no interfaces detected)

Router-side configuration

Configure your LAN's DHCP server to tag PXE clients with the options below. For UniFi: Settings → Networks → [your LAN] → Advanced → DHCP → Network Boot. For pfSense / OpenWRT / dnsmasq: equivalent options on the DHCP service.

{# Shorthand for "this host's IP", reused across both tables. Prefer the configured advertised host (withcache URL host), falling back to the sniffed primary interface. #} {% set bty_ip = suggested_host if suggested_host else (primary.ipv4 if primary and primary.ipv4 else "") %}

Option A — PXE (via TFTP)

Legacy BIOS and UEFI PXE. The firmware TFTPs the iPXE binary from this host, then iPXE chains on over HTTP.

Option Value Notes
option 60 PXEClient Vendor-class echo. Strict UEFI firmware filters offers without it.
option 66 / Next-Server {{ bty_ip }} The TFTP server. Same box as bty-web.
option 67 / Boot-Filename ipxe.efi For UEFI PXEClient (arch 6/7/9). Use undionly.kpxe for legacy BIOS (arch 0).
option 67 for user-class=iPXE http://{{ bty_ip }}:8080/pxe-bootstrap.ipxe Stops iPXE chain-loop. Different bootfile when iPXE re-DHCPs.

Option B — UEFI HTTP Boot (no TFTP)

For firmware with UEFI HTTP Boot, the target fetches iPXE directly over HTTP — no TFTP server in the path. Tag HTTP-Boot clients (vendor-class HTTPClient) with these instead of the PXE options above. Everything after iPXE loads is identical to the PXE flow.

Option Value Notes
option 60 HTTPClient Vendor-class echo. UEFI HTTP-Boot firmware filters offers without it (the HTTP-Boot counterpart of PXEClient).
option 66 / Next-Server {{ bty_ip }} Still required. bty's iPXE binary chains on to http://<next-server>:8080/pxe-bootstrap.ipxe, so this must point here even though the bootfile below is a full URL.
option 67 / Boot-Filename http://{{ bty_ip }}:8080/boot/ipxe.efi Full URL. The firmware HTTP-downloads iPXE directly — no TFTP roundtrip.

Both paths converge: once iPXE is running it fetches http://{{ bty_ip }}:8080/pxe-bootstrap.ipxe and then the per-MAC plan at /pxe/<mac>. The only differences are the vendor class and how the iPXE binary is delivered (TFTP vs HTTP).

{% endblock %}