{% 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. #} {# /ui/images was retired: withcache owns the catalog end-to-end. The old bty-side "Catalog URL" card lived here and pointed at a catalog.toml the /ui/images page fetched; both are gone. Point the operator at withcache instead. #}
Catalog {% if withcache_url_effective %} {# Sync button: forces WithcacheCatalog.refresh() at bty-web's end so a running server picks up entries the operator just Downloaded on withcache. Bty caches the snapshot at startup + refuses to auto-poll; without this button the only sync path was a container restart. #}
{% endif %}
The image catalog is served by withcache. Add / edit / remove entries on {% if withcache_url_effective %} {{ withcache_url_effective|trim('/') }}/ui/catalog {# Reachability probe result: green pill when /healthz returned 200 at render time, red pill otherwise. Blank URL renders no pill. Same pattern as the nbdmux pill in the Ramboot card below. #} {% if withcache_reachable is sameas true %} reachable {% elif withcache_reachable is sameas false %} unreachable {% endif %} . Bty caches the catalog snapshot in memory (refresh happens on startup and on demand via the button above); entries you Download on withcache after bty started are invisible to bty-web until you Sync. {% else %} the withcache server's /ui/catalog page. Set Withcache URL in the Ramboot card below (bty and nbdmux both consult the same value). {% endif %}
{# /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 here (set via ``[paths] backup_dir`` in bty.toml or ``BTY_PATHS_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 #} {# Display card: timezone the UI uses to render every operator-facing timestamp. bty stores in UTC; an empty value defaults to UTC. A bad IANA name redirects back with an error rather than persisting a broken value. #}
Display
IANA zone name (e.g. Europe/Copenhagen, America/New_York, UTC). All operator-facing timestamps in bty-web are converted from UTC (the storage standard) to this zone before display. Empty = revert to UTC. The change takes effect on the next page render; no restart needed. {% if display_timezone_override %} Currently effective: {{ display_timezone_effective }}. {% else %} Currently effective: {{ display_timezone_effective }} (default). {% endif %}
{# /row display #} {# Ramboot bytes path: where the in-place NBD multiplexer lives, and what overlay size targets get by default. Both knobs flow into the per-machine ramboot plan rendered by /pxe/{mac}/plan when boot_mode=ramboot. Leaving nbdmux_url empty disables ramboot entirely; the matching machine-edit dropdown rejects the option in that state. #}
Ramboot

Bytes path for the in-place nbdmux NBD multiplexer that serves catalog images over the network for the ramboot boot mode. Operator binds a machine to ramboot; bty-web decompresses the bound image once, registers it as an NBD export, and the target's initramfs mounts it with an overlayfs over tmpfs. See the ramboot-init bty-media variant for the initramfs side.

HTTP control plane of the nbdmux daemon (port 8082 by default). Empty disables ramboot; bty-web rejects ``boot_mode=ramboot`` bindings in that state. The NBD listener (port 10809) is derived from the same host. {% if nbdmux_url_effective %} Currently effective: {{ nbdmux_url_effective }} {# Reachability probe result: green pill when /healthz returned 200 at render time, red pill otherwise. Blank URL renders no pill (nothing to probe). Lets the operator spot a typo without submitting a ramboot bind and getting the 502. #} {% if nbdmux_reachable is sameas true %} reachable {% elif nbdmux_reachable is sameas false %} unreachable {% endif %} . {% else %} Currently effective: (none; ramboot unavailable). {% endif %}
Default tmpfs size for the in-target ramboot overlay (mount syntax: 10G, 8192M, ...). Operators can override per-machine from the machine-edit form. Empty reverts to {{ ramboot_overlay_size_default }}. Currently effective: {{ ramboot_overlay_size_effective }}.
{# /row ramboot #} {# 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 %}