furtka/webinstaller/app.py

550 lines
21 KiB
Python
Raw Permalink Normal View History

# ruff: noqa: E501 — inline HTML/CSS/JS payloads (_INDEX_HTML, _STYLE_CSS,
# _CADDYFILE, _FURTKA_STATUS_SH, etc.) round-trip verbatim to the installed
# system; wrapping them hurts readability and the rendered output is what
# matters.
import base64
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
import json
import os
import re
import subprocess
import sys
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
from pathlib import Path
from drives import list_scored_devices
from flask import Flask, jsonify, redirect, render_template, request, url_for
2026-04-13 19:38:34 +02:00
app = Flask(__name__)
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
LANGUAGES = {
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
"en": {"locale": "en_US.UTF-8", "label": "English", "keyboard": "us"},
"de": {"locale": "de_DE.UTF-8", "label": "Deutsch", "keyboard": "de"},
"pl": {"locale": "pl_PL.UTF-8", "label": "Polski", "keyboard": "pl"},
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
}
STATE_DIR = Path(os.environ.get("FURTKA_STATE_DIR", "/tmp/furtka"))
INSTALL_LOG = STATE_DIR / "install.log"
CONFIG_PATH = STATE_DIR / "user_configuration.json"
CREDS_PATH = STATE_DIR / "user_credentials.json"
# Pre-populated with sane defaults so the form has something useful on first
# render. POSTs validate and overwrite.
2026-04-13 19:38:34 +02:00
settings = {
"hostname": "furtka",
"username": "",
"password": "",
"language": "en",
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
"boot_drive": "",
2026-04-13 19:38:34 +02:00
}
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
HOSTNAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
USERNAME_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}$")
# Ordered phase markers for the install progress bar. Each tuple is
# (substring to search for in the archinstall log, progress percent when
# reached, user-facing label). Pick the furthest phase whose marker is
# present in the log. If archinstall changes its stdout wording the bar
# stalls on the last recognized phase — the install itself keeps going.
PROGRESS_PHASES = [
("Wiping partitions", 8, "Preparing your disk"),
("Creating partitions", 12, "Creating partitions"),
("Starting installation", 15, "Starting installation"),
("Waiting for", 18, "Syncing time and packages"),
("Installing packages: ['base'", 25, "Installing the base system (this takes a while)"),
("Adding bootloader", 65, "Setting up boot"),
("Installing packages: ['efibootmgr'", 70, "Setting up boot"),
("Installing packages: ['docker'", 80, "Installing your apps"),
("Enabling service", 90, "Turning on services"),
("Updating /mnt/etc/fstab", 95, "Almost done"),
("Installation completed without any errors", 100, "Done!"),
]
PROGRESS_ERROR_MARKERS = ("Traceback (most recent call last)", "archinstall: error:")
def parse_install_progress(log):
percent = 2
phase = "Starting up…"
for marker, pct, label in PROGRESS_PHASES:
if marker in log:
percent = pct
phase = label
if percent >= 100:
status = "done"
elif any(m in log for m in PROGRESS_ERROR_MARKERS):
status = "error"
phase = "Installation failed — open Show details below"
else:
status = "running"
return {"percent": percent, "phase": phase, "status": status}
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
def validate_step1(form):
errors = []
values = {
"hostname": form.get("hostname", "").strip(),
"username": form.get("username", "").strip(),
"password": form.get("password", ""),
"language": form.get("language", ""),
}
password2 = form.get("password2", "")
if not HOSTNAME_RE.match(values["hostname"]):
errors.append("Hostname must be lowercase letters, digits, hyphens (max 63 chars).")
if not USERNAME_RE.match(values["username"]):
errors.append("Username must start with a letter or underscore, lowercase only.")
if len(values["password"]) < 8:
errors.append("Password must be at least 8 characters.")
if values["password"] != password2:
errors.append("Passwords do not match.")
if values["language"] not in LANGUAGES:
errors.append("Pick a language.")
return errors, values
def build_disk_config(boot_drive):
# archinstall 4.x dropped the `use_entire_disk` shortcut — `default_layout`
# now requires fully-specified partitions. We call suggest_single_disk_layout
# with ext4 + no separate /home, which short-circuits its interactive prompts.
import asyncio
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.disk_menu import suggest_single_disk_layout
from archinstall.lib.models.device import (
DiskLayoutConfiguration,
DiskLayoutType,
FilesystemType,
)
device_handler.load_devices()
device = device_handler.get_device(Path(boot_drive))
if device is None:
raise RuntimeError(f"archinstall could not resolve device {boot_drive!r}")
device_mod = asyncio.run(
suggest_single_disk_layout(
device,
# archinstall renamed the enum members to ALL_CAPS at some point
# between when we wrote this and the pinned Arch live ISO version.
# The old name `Ext4` now raises AttributeError at install time.
filesystem_type=FilesystemType.EXT4,
separate_home=False,
)
)
layout = DiskLayoutConfiguration(
config_type=DiskLayoutType.Default,
device_modifications=[device_mod],
)
return layout.json()
# ---------------------------------------------------------------------------
# Post-install bootstrap payload
#
# Written into the target system via archinstall's `custom_commands` so that
# after reboot the user lands in "Furtka": Caddy serves a branded landing
# page + live status tiles on :80, avahi advertises proksi.local, and the
# console shows a welcome banner pointing at the URL.
#
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# Asset files (HTML, CSS, shell scripts, systemd units, Caddyfile) live in
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
# assets/ in the repo — at ISO build time they end up on the live ISO
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# as part of the webinstaller's source tree AND inside the resource-manager
# payload tarball. The installer reads them from the live-ISO copy, base64-
# encodes them, and hands them to archinstall so the chroot recreates each
# file bit-for-bit. Updates (Phase 2) refresh the tarball, which carries the
# same assets to the target's /opt/furtka/ tree.
# ---------------------------------------------------------------------------
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# Tarball built by iso/build.sh containing the furtka/ Python package + the
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
# bundled apps/ tree (plus assets/). The webinstaller reads it from the
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# live ISO at request-time and base64-encodes it into a custom_command for
# archinstall.
RESOURCE_MANAGER_PAYLOAD = Path("/opt/furtka-resource-manager.tar.gz")
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# Asset root. Two layouts we have to handle:
# dev / tests — webinstaller/app.py sits at repo_root/webinstaller/ and
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
# assets live at repo_root/assets/.
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# live ISO — iso/build.sh copies webinstaller/ to /opt/furtka/ AND
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
# copies assets/ to /opt/assets/ right next to
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
# app.py, so the same "assets next to me" lookup works.
# Probe the sibling path first (ISO case), fall back to the repo layout.
def _resolve_assets_dir() -> Path:
here = Path(__file__).resolve().parent
sibling = here / "assets"
if sibling.is_dir():
return sibling
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
repo_copy = here.parent / "assets"
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
if repo_copy.is_dir():
return repo_copy
raise FileNotFoundError(
f"furtka assets not found near {here} — looked in {sibling} and {repo_copy}"
)
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
_ASSETS_DIR = _resolve_assets_dir()
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
def _read_asset(relpath: str) -> str:
fix(furtka): move assets/ to repo top level so Caddy + systemd find it Root cause of today's 403 on a fresh install: assets/ lived inside the Python package at furtka/assets/, so the resource-manager tarball extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile has `root * /opt/furtka/current/assets/www`, systemd units point at /opt/furtka/current/assets/bin/furtka-status, and the install-time `systemctl link /opt/furtka/current/assets/systemd/*.service` expected the top-level layout. All three found nothing: - Caddy → 403 Forbidden (empty/missing document root) - systemctl link → silent no-op, nothing ever linked into /etc/systemd/system/ - furtka-api.service + furtka-reconcile.service → "inactive" because they were never registered Nothing in the Python package ever imported furtka.assets — these are shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is config data, not package data. Promoting assets/ to the repo root matches how it's referenced everywhere downstream and eliminates the path mismatch. Changes: - git mv furtka/assets assets - iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"` so the tarball ships ./assets at its root, and the live-ISO copy reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets. - scripts/build-release-tarball.sh: same for release tarballs. - webinstaller/app.py: _resolve_assets_dir's dev fallback walks one level up to REPO_ROOT/assets/. - tests/test_webinstaller_assets.py: ASSETS constant updated. Tests still green (150/150) because both paths were fs-level — no code imports changed. Next ISO build will land assets at the path everything downstream expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
"""Return the UTF-8 contents of an on-disk asset shipped under assets/.
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
Raises FileNotFoundError if the asset is missing, which is loud by design:
an install that tries to write an asset that isn't there is broken before
the user ever boots the target, not after.
"""
path = _ASSETS_DIR / relpath
return path.read_text(encoding="utf-8")
_FURTKA_WRAPPER_SH = """\
#!/bin/sh
# Tiny launcher for the furtka resource-manager CLI. The Python source lives
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# under /opt/furtka/current/furtka/ — /current is a symlink that gets
# flipped by self-updates (Phase 2), so this shim stays stable across
# upgrades while the underlying code tree is swapped atomically.
PYTHONPATH=/opt/furtka/current exec python3 -m furtka.cli "$@"
"""
def _write_file_cmd(path, content, mode=None):
"""Shell command that recreates `path` with `content` inside the chroot.
Uses base64 so we don't have to worry about bash / JSON / archinstall
quoting the payload through three layers of shell. `base64` is part of
coreutils and always available in the target system.
"""
b64 = base64.b64encode(content.encode()).decode()
parent = path.rsplit("/", 1)[0]
cmd = f"mkdir -p {parent} && printf %s {b64} | base64 -d > {path}"
if mode is not None:
cmd += f" && chmod {mode} {path}"
return cmd
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
_FURTKA_UNITS = (
"furtka-api.service",
"furtka-reconcile.service",
"furtka-status.service",
"furtka-status.timer",
"furtka-welcome.service",
)
def _resource_manager_commands():
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
"""Commands to land /opt/furtka/versions/<ver>/ + symlink /opt/furtka/current
+ the `furtka` CLI shim + systemctl-link the unit files.
Reads the payload tarball staged into the live ISO at build time. If the
file isn't present (dev box without an ISO build), returns [] so the rest
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
of the install still works the resource manager just won't be installed,
and nothing else on the system references furtka-* units.
"""
if not RESOURCE_MANAGER_PAYLOAD.exists():
print(
f"warning: {RESOURCE_MANAGER_PAYLOAD} missing, "
"resource manager will NOT be installed on target",
file=sys.stderr,
)
return []
payload_b64 = base64.b64encode(RESOURCE_MANAGER_PAYLOAD.read_bytes()).decode()
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# Extract to a staging directory first, then rename to versions/<ver>/.
# That way the version-ID lookup is data-driven (reads VERSION from the
# tarball) instead of hardcoded at install-time — keeps the installer
# version-agnostic so a newer ISO doesn't need a webinstaller change to
# ship a new Furtka version.
extract_and_link = (
"mkdir -p /opt/furtka/versions && "
"staging=$(mktemp -d /opt/furtka/versions/staging-XXXXXX) && "
f'printf %s {payload_b64} | base64 -d | tar -xzf - -C "$staging" && '
'ver=$(cat "$staging/VERSION") && '
fix(furtka): pre-ISO audit fixes — chmod, Caddyfile refresh, unit linking Five issues surfaced by the Phase-2 audit before the next ISO rebuild: P1 (real blockers for a fresh install / self-update): 1. chmod +x furtka/assets/bin/furtka-status, furtka-welcome. They were mode 644 in git, so the tarball shipped them non-executable and every ExecStart referencing /opt/furtka/current/assets/bin/furtka-* would have failed on first boot with Permission denied. 2. apply_update now refreshes /etc/caddy/Caddyfile from the new version when the content differs, then reloads caddy. Without this, a release that changes Caddy routes silently stays on the old config. 3. apply_update now systemctl-links any new unit files shipped by the update, not just the five linked at install time. A future release that adds furtka-foo.service would otherwise never appear in /etc/systemd/system/. P2 (hardening, not blockers today): 4. _resource_manager_commands now aborts the install if the tarball's VERSION file is empty — otherwise `mv "$staging" /opt/furtka/versions/` would move the staging dir in as a subdirectory and the symlink target would be invalid. 5. _extract_tarball passes filter='data' to tarfile.extractall on Python 3.12+ to catch symlink-escape / setuid / device-node tricks that the regex path-check can't see. Falls back silently on older interpreters. Plus the CHANGELOG [Unreleased] section got filled in with the whole Phase-1 + Phase-2 + UI-uplevel body so a 26.1-alpha tag cut off main has meaningful release notes. Test additions / updates: - test_refresh_caddyfile_{copies_when_different,noops_if_source_missing} - test_link_new_units_only_links_missing - test_extract_tarball_uses_data_filter_when_available - test_apply_update_happy_path now verifies the Caddyfile gets copied. - test_resource_manager_extracts_to_versioned_slot verifies the empty-VERSION guard is present in the install command. Paths now overridable via FURTKA_CADDYFILE_PATH + FURTKA_SYSTEMD_DIR so tests can pin a tmpdir for these new fs operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:10:07 +02:00
# Guard against an empty VERSION file: without this, `mv "$staging"
# "/opt/furtka/versions/"` would move the staging dir into versions/
# as a subdir and the symlink target would be invalid.
'[ -n "$ver" ] || { echo "empty VERSION in payload" >&2; exit 1; } && '
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
'mv "$staging" "/opt/furtka/versions/$ver" && '
# mktemp -d creates the staging dir with mode 700; that survives the
# mv and leaves Caddy (which runs as the `caddy` user, not root)
# unable to traverse /opt/furtka/current/ when it tries to serve
# the landing page. Open up to 755 so file_server can read.
'chmod 755 "/opt/furtka/versions/$ver" && '
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current'
)
systemctl_link = "systemctl link " + " ".join(
f"/opt/furtka/current/assets/systemd/{u}" for u in _FURTKA_UNITS
)
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
systemctl_enable = "systemctl enable " + " ".join(_FURTKA_UNITS)
return [
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
extract_and_link,
_write_file_cmd("/usr/local/bin/furtka", _FURTKA_WRAPPER_SH, mode="755"),
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
systemctl_link,
systemctl_enable,
]
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
def _furtka_json_cmd(hostname):
"""Write /var/lib/furtka/furtka.json with install-time facts.
Replaces the __HOSTNAME__ sed pass the landing page reads this file
at runtime and renders the hostname chip from it. install_date + version
ride along so the settings page can display them without hitting the
status timer's refresh cycle.
Heredoc rather than base64 + sed the previous version had two layers
of quoting that archinstall's custom_commands shell-eval path parsed
inconsistently, leaving this command as a silent no-op on some installs.
The heredoc evaluates `$(date ...)` and `$(cat VERSION)` at chroot
runtime and sidesteps the quoting hazard entirely. Hostname has already
been validated by validate_step1.
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
"""
return (
"mkdir -p /var/lib/furtka && "
"cat > /var/lib/furtka/furtka.json <<EOF\n"
"{\n"
f' "hostname": "{hostname}",\n'
' "install_date": "$(date -Iseconds)",\n'
' "version": "$(cat /opt/furtka/current/VERSION 2>/dev/null || echo dev)"\n'
"}\n"
"EOF"
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
)
def _post_install_commands(hostname):
# nss-mdns: splice `mdns_minimal [NOTFOUND=return]` before `resolve` on
# the hosts line so `*.local` works from the installed system too. Guarded
# so a re-run (or a future Arch default that already ships mdns) is a
# no-op instead of double-injecting.
nss_sed = (
"grep -q 'mdns_minimal' /etc/nsswitch.conf || "
"sed -i '/^hosts:/ s/resolve/mdns_minimal [NOTFOUND=return] resolve/' "
"/etc/nsswitch.conf"
)
return [
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# The Caddyfile lives at /etc/caddy/Caddyfile per Caddy's convention
# (systemd unit points there). Content comes from the shipped asset,
# which we copy in at install time so updates that change routing
# need a new release to refresh it.
refactor(webinstaller): extract inline payload constants to furtka/assets/ Slice 1a of the self-update story. Every HTML/CSS/shell-script/systemd- unit payload that used to live as a triple-quoted string constant inside webinstaller/app.py now lives as a real file under furtka/assets/: furtka/assets/Caddyfile furtka/assets/VERSION (new — matches pyproject.toml) furtka/assets/www/{index.html, settings/index.html, style.css, status.json} furtka/assets/bin/{furtka-status, furtka-welcome} furtka/assets/systemd/furtka-{api,reconcile,status,welcome}.service furtka/assets/systemd/furtka-status.timer The installer now pulls each file from disk via _read_asset(). Byte-for- byte identical output at install time — a fresh-ISO install should land the same files in the same places with the same contents, verified by tests/test_webinstaller_assets.py which reconstructs each base64 blob and asserts equality against the on-disk asset. iso/build.sh also copies furtka/assets/ next to the webinstaller source at /opt/furtka/assets on the live ISO so _resolve_assets_dir() finds them with a "next to me" lookup. In dev the same function walks two levels up to the repo copy, so pytest works without any env vars. furtka-status.sh drops the /etc/furtka/version TODO — it now reads /opt/furtka/VERSION directly, which Slice 1b will upgrade to /opt/furtka/current/VERSION once the symlink layout lands. _FURTKA_WRAPPER_SH (the 5-line /usr/local/bin/furtka shim) stays inline; it's tiny and not asset-shaped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:08:53 +02:00
_write_file_cmd("/etc/caddy/Caddyfile", _read_asset("Caddyfile")),
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# Initial status.json so Caddy doesn't 404 before furtka-status fires.
_write_file_cmd("/var/lib/furtka/status.json", _read_asset("www/status.json")),
nss_sed,
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# Resource manager bootstrap: extract tarball → versions/<ver>/,
# symlink current, install wrapper, systemctl-link unit files.
*_resource_manager_commands(),
feat(furtka): serve from /opt/furtka/current, retire /srv/furtka/www/ Slice 1b of the self-update story. The installer now sets up a versioned layout — install extracts the resource-manager tarball to a staging dir, reads the VERSION it contains, moves the dir to /opt/furtka/versions/<ver>/, and creates /opt/furtka/current as a symlink pointing at it. All runtime references (Caddy, wrapper, systemd ExecStart) go through /current, so Phase 2's self-update just flips the symlink atomically. Systemd units move from hand-written files in /etc/systemd/system/ to `systemctl link /opt/furtka/current/assets/systemd/*` — one link per unit, stable across upgrades because the link target is /current. The furtka-status + furtka-welcome units now ExecStart the shipped scripts directly from /opt/furtka/current/assets/bin/, which means we no longer copy those scripts to /usr/local/bin/ at install time. Runtime JSON (status.json, furtka.json, update-state.json) moves to /var/lib/furtka/ so self-updates never clobber it. Caddy serves those three paths from there; everything else from /opt/furtka/current/assets/www/. The __HOSTNAME__ sed-template hack is gone. At install time we write /var/lib/furtka/furtka.json with {hostname, install_date, version}, and the landing page's JS reads it on load to populate the hostname chip and to build the SMB deep-link for the fileshare tile. First paint gets a "—" placeholder and hydrates once fetch completes. Test updates: - test_webinstaller_assets enforces the new command shape (extract-to- staging, ln -sfn /opt/furtka/current, systemctl link per unit, no writes to /srv/furtka/www/). - test_app's legacy "payload present" / "payload absent" tests match the new layout too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:15:59 +02:00
# furtka.json depends on /opt/furtka/current/VERSION, so it has to
# run after the resource-manager commands.
_furtka_json_cmd(hostname),
]
def _detect_bootloader():
# systemd-boot is UEFI-only; on BIOS/legacy it trips HardwareIncompatibilityError
# inside archinstall. /sys/firmware/efi exists iff we were booted via UEFI.
return "Systemd-boot" if Path("/sys/firmware/efi").exists() else "Grub"
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
def build_archinstall_config(s):
return {
"archinstall-language": "English",
"timezone": "Europe/Berlin",
"ntp": True,
"bootloader": _detect_bootloader(),
"disk_config": build_disk_config(s["boot_drive"]),
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
"hostname": s["hostname"],
"kernels": ["linux"],
"packages": [
"docker",
"docker-compose",
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
# Editors for console/SSH recovery — `nano` is the beginner-friendly
# one, `vim` stays because it's muscle-memory for the dev team.
"nano",
"vim",
"git",
"htop",
"curl",
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
# Remote access — archinstall 4.x's `ssh: True` flag is flaky about
# actually pulling in openssh, so list it explicitly and enable sshd
# via `services` below. Without this, the documented recovery path
# (SSH in → edit .env) doesn't work.
"openssh",
# Base OS post-install (landing page + mDNS on installed system).
"caddy",
"avahi",
"nss-mdns",
# Resource manager runtime — pure-stdlib Python, no pip needed
# because we expose the package via PYTHONPATH in /usr/local/bin/furtka.
"python",
],
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
"profile": {"type": "server"},
"services": [
"docker",
# Base OS post-install services. Only packaged units go here —
# archinstall runs `systemctl enable` on this list *before*
# custom_commands, so our own furtka-welcome + furtka-status.timer
# units (written in custom_commands) are enabled there instead.
"caddy",
"avahi-daemon",
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
"sshd",
],
# `gpasswd -a <user> docker` has to stay first — adds the user to
# the docker group once the group exists (archinstall creates users
# before pacstrapping extras). After that we drop the Furtka landing
# page, status timer, and welcome banner into place.
"custom_commands": [
f"gpasswd -a {s['username']} docker",
*_post_install_commands(s["hostname"]),
],
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
"network_config": {"type": "iso"},
"ssh": True,
"audio_config": None,
"locale_config": {
"locale": LANGUAGES[s["language"]]["locale"],
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
# Keyboard layout follows the chosen language so a German user
# doesn't get a US layout at the TTY console (where things like
# `/`, `-`, `=` land on surprising keys and make even `sudo vim`
# painful). `en` falls through to "us" which is what we want.
"keyboard_layout": LANGUAGES[s["language"]]["keyboard"],
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
},
}
def build_archinstall_creds(s):
# archinstall 4.x expects `!root-password` and `!password` (plaintext
# sentinels). Users with neither `!password` nor `enc_password` are
# silently dropped by User.parse_arguments — hence login failures.
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
return {
"!root-password": s["password"],
"users": [
{
"username": s["username"],
"!password": s["password"],
"sudo": True,
"groups": [],
}
],
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
}
def write_install_files(s, state_dir):
state_dir.mkdir(parents=True, exist_ok=True)
config_path = state_dir / "user_configuration.json"
creds_path = state_dir / "user_credentials.json"
config_path.write_text(json.dumps(build_archinstall_config(s), indent=2))
creds_path.write_text(json.dumps(build_archinstall_creds(s), indent=2))
creds_path.chmod(0o600)
return config_path, creds_path
def spawn_archinstall(config_path, creds_path, log_path):
log_fh = open(log_path, "wb")
return subprocess.Popen(
[
"archinstall",
"--config",
str(config_path),
"--creds",
str(creds_path),
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
"--silent",
],
stdout=log_fh,
stderr=subprocess.STDOUT,
start_new_session=True,
)
2026-04-13 19:38:34 +02:00
@app.route("/")
def home():
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
return redirect(url_for("install_step_1"))
2026-04-13 19:38:34 +02:00
2026-04-13 19:38:34 +02:00
@app.route("/install/step1", methods=["GET", "POST"])
def install_step_1():
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
errors = []
2026-04-13 19:38:34 +02:00
if request.method == "POST":
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
errors, values = validate_step1(request.form)
if not errors:
settings.update(values)
return redirect(url_for("install_step_2"))
form_values = values
else:
form_values = {k: settings[k] for k in ("hostname", "username", "language")}
return render_template(
"install/step1.html",
values=form_values,
languages=LANGUAGES,
errors=errors,
)
2026-04-13 19:38:34 +02:00
@app.route("/install/step2", methods=["GET", "POST"])
def install_step_2():
if request.method == "POST":
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
boot_drive = request.form.get("boot_drive", "").strip()
if boot_drive:
settings["boot_drive"] = boot_drive
return redirect(url_for("install_overview"))
return render_template(
"install/step2.html",
drives=list_scored_devices(),
selected=settings.get("boot_drive", ""),
)
2026-04-13 19:38:34 +02:00
@app.route("/install/overview")
def install_overview():
feat: webinstaller writes archinstall config + execs install, styled Wires the live-ISO wizard from "shows three screens" to "actually invokes archinstall on the chosen disk", plus first-pass styling so it stops looking like raw <h1>/<form>. Webinstaller flow: - S1 form gains username/password/password2/language with server-side validation (hostname/username regex, ≥8 char password, match check). - /install/run writes user_configuration.json + user_credentials.json (creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs `archinstall --config … --creds … --silent` as a backgrounded subprocess. - /install/log renders the subprocess output via meta-refresh polling. - FURTKA_DRY_RUN=1 short-circuits the exec for testing. - archinstall flag names verified against `archinstall --help` in an archlinux container before committing. Drive list: - drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk, so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing as install targets. Boot menu: - iso/build.sh sed-rebrands "Arch Linux install medium" → "Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/ entries. Verified zero leftovers against the current releng profile. Styling: - static/style.css adopts the website's design tokens (palette, typography, gate-mark accent), with light + dark via prefers-color-scheme. - New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step indicator) and footer; all install templates extend it. - Drive picker uses radio cards with score chip; overview uses a summary table and a destructive "wipe drive" button. Tests: 17 pass (4 new in test_app.py covering validation + config builders, 2 new in test_drives.py covering the lsblk filter). Ruff clean. README roadmap updated to mark these done and explicitly defer the 26.0-alpha release until archinstall actually completes end-to-end on a VM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
masked = {**settings, "password": "" * 8 if settings["password"] else ""}
return render_template("install/overview.html", settings=masked)
@app.route("/install/run", methods=["POST"])
def install_run():
if not settings["boot_drive"] or not settings["username"] or not settings["password"]:
return redirect(url_for("install_step_1"))
config_path, creds_path = write_install_files(settings, STATE_DIR)
INSTALL_LOG.write_bytes(b"")
if os.environ.get("FURTKA_DRY_RUN") == "1":
INSTALL_LOG.write_text(
f"DRY RUN: would exec archinstall --config {config_path} "
f"--creds {creds_path} --silent\n"
)
else:
spawn_archinstall(config_path, creds_path, INSTALL_LOG)
return redirect(url_for("install_log_view"))
@app.route("/install/log")
def install_log_view():
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else ""
return render_template(
"install/log.html",
log=log,
progress=parse_install_progress(log),
)
2026-04-13 19:38:34 +02:00
@app.route("/install/log.json")
def install_log_json():
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else ""
return jsonify(log=log, progress=parse_install_progress(log))
@app.route("/install/reboot", methods=["POST"])
def install_reboot():
# Only allow rebooting once the install has actually finished — we don't
# want a panicked click during install to reboot mid-pacstrap.
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else ""
if parse_install_progress(log)["status"] != "done":
return redirect(url_for("install_log_view"))
# Delay reboot a few seconds so the browser can finish fetching CSS / assets
# for the rebooting page before the Flask server (and network) go away.
# Without this, the reboot page renders unstyled (giant inline SVG icon).
subprocess.Popen(
["/bin/sh", "-c", "sleep 3 && /usr/bin/systemctl reboot"],
start_new_session=True,
)
return render_template("install/rebooting.html", hostname=settings["hostname"])
2026-04-13 19:38:34 +02:00
if __name__ == "__main__":
app.run(debug=True, port=5000)