furtka/webinstaller/app.py

648 lines
25 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(auth): login-guard the Furtka UI with a cookie session One-admin, one-password model — all of /apps, /api/*, /, and /settings/ now require a signed-in session. Passwords are werkzeug PBKDF2-hashed in /var/lib/furtka/users.json (mode 0600, atomic write via the same .tmp+chmod+rename dance installer.write_env uses). Sessions are secrets.token_urlsafe(32) tokens held in a module-level SessionStore dict (thread-safe lock included for when we swap to ThreadingHTTPServer). Cookies are HttpOnly, SameSite=Strict, and Path=/, with Secure set when X-Forwarded-Proto from Caddy says HTTPS. Two bootstrap paths: * Fresh install — webinstaller step-1 collects Linux user + password, the chroot post-install step hashes the password and writes users.json on the target partition. First browser visit lands on /login with the account already present. * Upgrade from 26.10-alpha — no users.json yet, so /login detects setup_needed() and renders a first-run setup form. POST creates the admin and immediately logs in. POST /logout revokes the server session and clears the cookie. Unauthenticated HTML requests 302 to /login; unauthenticated API requests 401 JSON so fetch() callers see a clean error. A sleep(0.5) on failed logins is the brute-force speed bump on top of werkzeug's ~600k-iter PBKDF2. Caddyfile gains /login* and /logout* handle blocks in the shared furtka_routes snippet so both :80 and the HTTPS hostname block forward the auth endpoints to localhost:7000. Without this Caddy would 404 from the static file server. Test surface: * tests/test_auth.py (new, 19 cases): hash roundtrip, users.json I/O, session create/lookup/expire/revoke. * tests/test_api.py: new admin_session fixture; existing HTTP tests updated to send the cookie; new tests cover login setup, login success, wrong-password 401, logout revocation, and the guard's 302/401 split. * tests/test_webinstaller_assets.py: new case that unpacks the users.json _write_file_cmd body and verifies the werkzeug hash round-trips against the step-1 password. Bumped version to 26.11-alpha and rolled CHANGELOG. Also folded in the ruff-format fix that was pending from 26.10-alpha's lint red. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:01:17 +02:00
from datetime import UTC
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__)
def _resolve_version() -> str:
"""Resolve the Furtka version to display in the wizard footer.
On the live ISO `iso/build.sh` writes `/opt/furtka/VERSION` at build time
from `pyproject.toml`; that's the authoritative source at runtime. For
local dev runs (pytest, `flask run` outside the ISO) fall back to
reading `pyproject.toml` directly, then to the literal "dev" so the
footer never 500s if both files are missing.
"""
iso_path = Path(__file__).resolve().parent / "VERSION"
for candidate in (iso_path, Path(__file__).resolve().parent.parent / "pyproject.toml"):
try:
text = candidate.read_text(encoding="utf-8")
except (FileNotFoundError, PermissionError, OSError):
continue
if candidate.name == "VERSION":
value = text.strip()
if value:
return value
else:
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
if match:
return match.group(1)
return "dev"
FURTKA_VERSION = _resolve_version()
@app.context_processor
def _inject_version():
return {"furtka_version": FURTKA_VERSION}
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",
feat(catalog): on-box apps catalog synced independently of core version New `furtka catalog sync` pulls the latest daniel/furtka-apps release, verifies its sha256, extracts under /var/lib/furtka/catalog/, and atomically swaps into place — so apps can ship without cutting a new Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a manual "Sync apps catalog" button that kicks the same code path via a detached systemd-run unit. Layout of the new on-box tree: /var/lib/furtka/catalog/ synced catalog (survives self-updates) ├── VERSION └── apps/<name>/ ... /var/lib/furtka/catalog-state.json sync stage + last version, UI-polled /run/furtka/catalog.lock flock so timer + manual click can't race Resolver precedence (furtka/sources.py): catalog wins over the bundled seed (/opt/furtka/current/apps/, carried by the core release for offline first-boot). Installed apps under /var/lib/furtka/apps/ are never auto- swapped — user clicks Reinstall to move an existing install onto a newer catalog version; settings merge-preserved via the existing installer.install_from path. New files: - furtka/_release_common.py — shared Forgejo/tarball primitives lifted from furtka/updater.py. Both modules now import from here; updater's behaviour and public API unchanged. - furtka/catalog.py — check_catalog(), sync_catalog() with staging + manifest validation + atomic rename. Refuses bad sha256 / broken manifests and leaves the live catalog intact on any failure path. - furtka/sources.py — resolve_app_name() / list_available() abstraction used by installer.resolve_source and api._list_available. - assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service + daily timer. Timer auto-enables on self-update via a one-line addition to _link_new_units (fresh installs get enabled via the webinstaller's _FURTKA_UNITS list). API + UI: - /api/bundled renamed internally to _list_available; endpoint stays as a backcompat alias; /api/apps/available is the new canonical name. Each list entry carries a `source` field ("catalog" | "bundled"). - POST /api/catalog/sync/check + /apply + GET /api/catalog/status. - /apps page grows a catalog-status row + Sync button; poll loop mirrors the Furtka self-update flow. CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both support --json). Old `furtka app install` / `reconcile` / `update` / `rollback` surfaces are unchanged. Test gate: 194/170 baseline + 24 new tests covering catalog sync (happy path, sha256 mismatch, invalid manifest, lock contention, preserves-on-failure) + resolver precedence + api renames. ruff check + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
# Daily apps-catalog pull. Timer drives the service; the .service itself
# is oneshot and also callable ad-hoc via `furtka catalog sync`.
"furtka-catalog-sync.service",
"furtka-catalog-sync.timer",
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 _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
)
feat(auth): login-guard the Furtka UI with a cookie session One-admin, one-password model — all of /apps, /api/*, /, and /settings/ now require a signed-in session. Passwords are werkzeug PBKDF2-hashed in /var/lib/furtka/users.json (mode 0600, atomic write via the same .tmp+chmod+rename dance installer.write_env uses). Sessions are secrets.token_urlsafe(32) tokens held in a module-level SessionStore dict (thread-safe lock included for when we swap to ThreadingHTTPServer). Cookies are HttpOnly, SameSite=Strict, and Path=/, with Secure set when X-Forwarded-Proto from Caddy says HTTPS. Two bootstrap paths: * Fresh install — webinstaller step-1 collects Linux user + password, the chroot post-install step hashes the password and writes users.json on the target partition. First browser visit lands on /login with the account already present. * Upgrade from 26.10-alpha — no users.json yet, so /login detects setup_needed() and renders a first-run setup form. POST creates the admin and immediately logs in. POST /logout revokes the server session and clears the cookie. Unauthenticated HTML requests 302 to /login; unauthenticated API requests 401 JSON so fetch() callers see a clean error. A sleep(0.5) on failed logins is the brute-force speed bump on top of werkzeug's ~600k-iter PBKDF2. Caddyfile gains /login* and /logout* handle blocks in the shared furtka_routes snippet so both :80 and the HTTPS hostname block forward the auth endpoints to localhost:7000. Without this Caddy would 404 from the static file server. Test surface: * tests/test_auth.py (new, 19 cases): hash roundtrip, users.json I/O, session create/lookup/expire/revoke. * tests/test_api.py: new admin_session fixture; existing HTTP tests updated to send the cookie; new tests cover login setup, login success, wrong-password 401, logout revocation, and the guard's 302/401 split. * tests/test_webinstaller_assets.py: new case that unpacks the users.json _write_file_cmd body and verifies the werkzeug hash round-trips against the step-1 password. Bumped version to 26.11-alpha and rolled CHANGELOG. Also folded in the ruff-format fix that was pending from 26.10-alpha's lint red. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:01:17 +02:00
def _users_json_cmd(username, password):
"""Write /var/lib/furtka/users.json with the admin account hashed.
The core furtka-api reads this file on every login attempt; the
auth.py module treats `admin.username` + `admin.hash` as the only
credential. Hashing happens here in the webinstaller (werkzeug is a
flask transitive dep so it's already installed in this environment)
the chroot doesn't need pip. Mode 0600 so nobody but root on the
installed box can read the PBKDF2 hash.
"""
from datetime import datetime
from werkzeug.security import generate_password_hash
users = {
"admin": {
"username": username,
"hash": generate_password_hash(password),
"created_at": datetime.now(UTC).isoformat(timespec="seconds"),
}
}
return _write_file_cmd(
"/var/lib/furtka/users.json",
json.dumps(users, indent=2) + "\n",
mode="600",
)
def _post_install_commands(hostname, admin_username, admin_password):
# 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 [
# Import dir for the HTTP→HTTPS force-redirect snippet. The
# /api/furtka/https/force endpoint writes/removes a .caddyfile here
# to toggle the redirect. Must exist before Caddy starts — the
# Caddyfile's glob `import /etc/caddy/furtka.d/*.caddyfile` tolerates
# an empty dir but not a missing one on every Caddy version, so we
# create it up front and stay on the safe side.
"install -d -m 0755 -o root -g root /etc/caddy/furtka.d",
fix(https): make HTTPS opt-in to stop the BAD_SIGNATURE trap on fresh installs Every Furtka since 26.5 shipped a Caddyfile with a `__FURTKA_HOSTNAME__.local { tls internal }` site block, so every first boot auto-generated a fresh self-signed CA + intermediate + leaf. That worked for the first-ever Furtka user, but every reinstall (or second box on the same LAN) produced a new CA whose intermediate shared the fixed CN `Caddy Local Authority - ECC Intermediate` with the previous one. Firefox caches intermediates by CN across profiles — even private windows share cert9.db — so any visitor who had trusted an older Furtka's CA got a cached intermediate with mismatched keys when they hit the new box, producing `SEC_ERROR_BAD_SIGNATURE`. Unlike UNKNOWN_ISSUER, Firefox has NO "Advanced → Accept Risk" bypass for BAD_SIGNATURE, so fresh-install boxes were effectively unreachable over HTTPS in any browser that had ever seen a previous Furtka. Validated live on the .46 test VM: fresh 26.14 ISO install → Firefox hits BAD_SIGNATURE on https://furtka.local/ (even in private mode). Chromium bypasses it via mDNS failure but the issue is the same. openssl verify on the box confirms the chain is internally valid — this is purely client-side cache pollution across boxes. Fix: - assets/Caddyfile: removed the hostname site block. Default install serves :80 only — https://furtka.local connection-refuses, which is a normal error every browser handles instead of the unbypassable crypto fault. Added top-level import of /etc/caddy/furtka-https.d/*.caddyfile so the /settings HTTPS toggle can drop a listener snippet there when a user explicitly opts in. - furtka/https.py: set_force_https now writes TWO snippets atomically — the top-level hostname + tls internal block (enables :443) and the :80-scoped redirect (forces HTTP→HTTPS). Disable removes both. Reload failure rolls both back. Added _read_hostname + _https_snippet_content helpers with `/etc/hostname` → 'furtka' fallback so a missing hostname file doesn't produce an empty site block Caddy rejects. - furtka/https.py::status: force_https now reads the listener snippet (was reading the redirect snippet). A redirect without a listener isn't actually HTTPS being served, so the listener is the authoritative "HTTPS is on" signal. - furtka/updater.py: new _maybe_migrate_preserve_https hook runs inside _refresh_caddyfile on the 26.14 → 26.15 transition. If the box had the redirect snippet on disk (user had opted into HTTPS under the old regime), it writes the new listener snippet too so HTTPS keeps working after the Caddyfile swap removes the hostname block. - webinstaller/app.py: post-install creates /etc/caddy/furtka-https.d/ alongside /etc/caddy/furtka.d/ so the glob import can't trip an older Caddy on a missing path during the first reload. Live-tested on .46: set_force_https(True) writes both snippets, Caddy reloads, :443 listener comes up with fresh CA, curl -k returns 302, HTTP 301-redirects. set_force_https(False) removes both snippets atomically, :443 goes back to connection-refused. Tests: test_https.py expanded from 13 to 15 cases. Toggle-on asserts both snippets written + hostname substituted. Toggle-off asserts both removed. Rollback cases verify BOTH snippets restore on reload failure. New test_https_snippet_content_has_tls_internal_and_routes locks the exact shape of the listener block. test_webinstaller_assets.py: updated two old asserts that assumed hostname block was in Caddyfile; new test_post_install_creates_https_snippet_dir guards the new directory. 276 tests pass, ruff check + format clean. Known remaining wart (documented in CHANGELOG): a browser that trusted a prior Furtka CA still hits BAD_SIGNATURE on this box's HTTPS after enabling it, because the fixed intermediate CN is a Caddy-side limitation. Workaround: clear cert9.db or visit in a fresh profile. Won't affect end users with one Furtka box ever. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:30:04 +02:00
# Parallel dir for the top-level HTTPS-listener snippet, written
# by /api/furtka/https/force (26.15-alpha+) when the user opts
# into HTTPS. Empty by default so fresh installs never generate
# a tls internal cert — that was the 26.14 regression where
# Firefox hit unbypassable SEC_ERROR_BAD_SIGNATURE because
# Caddy's fixed intermediate-CN clashed with any cached trust
# from a previously-reinstalled Furtka box.
"install -d -m 0755 -o root -g root /etc/caddy/furtka-https.d",
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.
fix(https): restore TLS handshake — name hostname + correct PKI path Closes #10. Two linked bugs in 26.4-alpha's Phase 1 HTTPS made the force-HTTPS toggle fatal: every SNI handshake on :443 died with SSL_ERROR_INTERNAL_ERROR_ALERT, so the toggle redirected users from working HTTP to broken HTTPS. Root cause 1: bare `:443 { tls internal }` gives Caddy no hostname to issue a leaf cert for, so /var/lib/caddy/certificates/ stayed empty and Caddy sent TLS `internal_error` on every handshake. Fix: the :443 block is now `__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ { tls internal }`, with the marker substituted by webinstaller/app.py at install time and by furtka.updater._refresh_caddyfile on self-update (reads /etc/hostname, falls back to "furtka"). `auto_https disable_redirects` keeps Caddy's built-in redirect out of the way of the /settings toggle. Root cause 2: furtka/https.py and the /rootCA.crt handler both referenced /var/lib/caddy/.local/share/caddy/pki/authorities/local/ — a path that doesn't exist. caddy.service sets XDG_DATA_HOME=/var/lib, so Caddy's storage is /var/lib/caddy/ directly. Fix: both paths corrected. Verified on the 192.168.178.110 smoke VM by swapping the Caddyfile in, reloading, handshaking, restoring: TLS 1.3 handshake succeeds, leaf cert issued under /var/lib/caddy/certificates/local/, /rootCA.crt returns 200. Tests: new cases assert the Caddyfile ships the hostname placeholder, the webinstaller substitutes it, _refresh_caddyfile re-substitutes from /etc/hostname on update, and the asset sets auto_https disable_redirects. Unit tests still stub the Caddy reload — the real handshake regression needs a smoke-VM integration test (follow-up, separate from this fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:38:16 +02:00
#
# __FURTKA_HOSTNAME__ is the placeholder the asset carries in place
# of the real hostname — Caddy's `tls internal` needs a named site
# block to issue a leaf cert, and the hostname isn't known until
# the user fills in the form. Self-updates re-apply the same
# substitution against /etc/hostname (see updater._refresh_caddyfile).
_write_file_cmd(
"/etc/caddy/Caddyfile",
_read_asset("Caddyfile").replace("__FURTKA_HOSTNAME__", hostname),
),
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),
feat(auth): login-guard the Furtka UI with a cookie session One-admin, one-password model — all of /apps, /api/*, /, and /settings/ now require a signed-in session. Passwords are werkzeug PBKDF2-hashed in /var/lib/furtka/users.json (mode 0600, atomic write via the same .tmp+chmod+rename dance installer.write_env uses). Sessions are secrets.token_urlsafe(32) tokens held in a module-level SessionStore dict (thread-safe lock included for when we swap to ThreadingHTTPServer). Cookies are HttpOnly, SameSite=Strict, and Path=/, with Secure set when X-Forwarded-Proto from Caddy says HTTPS. Two bootstrap paths: * Fresh install — webinstaller step-1 collects Linux user + password, the chroot post-install step hashes the password and writes users.json on the target partition. First browser visit lands on /login with the account already present. * Upgrade from 26.10-alpha — no users.json yet, so /login detects setup_needed() and renders a first-run setup form. POST creates the admin and immediately logs in. POST /logout revokes the server session and clears the cookie. Unauthenticated HTML requests 302 to /login; unauthenticated API requests 401 JSON so fetch() callers see a clean error. A sleep(0.5) on failed logins is the brute-force speed bump on top of werkzeug's ~600k-iter PBKDF2. Caddyfile gains /login* and /logout* handle blocks in the shared furtka_routes snippet so both :80 and the HTTPS hostname block forward the auth endpoints to localhost:7000. Without this Caddy would 404 from the static file server. Test surface: * tests/test_auth.py (new, 19 cases): hash roundtrip, users.json I/O, session create/lookup/expire/revoke. * tests/test_api.py: new admin_session fixture; existing HTTP tests updated to send the cookie; new tests cover login setup, login success, wrong-password 401, logout revocation, and the guard's 302/401 split. * tests/test_webinstaller_assets.py: new case that unpacks the users.json _write_file_cmd body and verifies the werkzeug hash round-trips against the step-1 password. Bumped version to 26.11-alpha and rolled CHANGELOG. Also folded in the ruff-format fix that was pending from 26.10-alpha's lint red. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:01:17 +02:00
# Admin account for the Furtka web UI. Hashed here (werkzeug is
# already in scope for the Flask webinstaller) and materialised
# into /var/lib/furtka/users.json at mode 0600 on the target
# partition — the installed core's auth.py picks it up on first
# login.
_users_json_cmd(admin_username, admin_password),
]
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",
feat(auth): login-guard the Furtka UI with a cookie session One-admin, one-password model — all of /apps, /api/*, /, and /settings/ now require a signed-in session. Passwords are werkzeug PBKDF2-hashed in /var/lib/furtka/users.json (mode 0600, atomic write via the same .tmp+chmod+rename dance installer.write_env uses). Sessions are secrets.token_urlsafe(32) tokens held in a module-level SessionStore dict (thread-safe lock included for when we swap to ThreadingHTTPServer). Cookies are HttpOnly, SameSite=Strict, and Path=/, with Secure set when X-Forwarded-Proto from Caddy says HTTPS. Two bootstrap paths: * Fresh install — webinstaller step-1 collects Linux user + password, the chroot post-install step hashes the password and writes users.json on the target partition. First browser visit lands on /login with the account already present. * Upgrade from 26.10-alpha — no users.json yet, so /login detects setup_needed() and renders a first-run setup form. POST creates the admin and immediately logs in. POST /logout revokes the server session and clears the cookie. Unauthenticated HTML requests 302 to /login; unauthenticated API requests 401 JSON so fetch() callers see a clean error. A sleep(0.5) on failed logins is the brute-force speed bump on top of werkzeug's ~600k-iter PBKDF2. Caddyfile gains /login* and /logout* handle blocks in the shared furtka_routes snippet so both :80 and the HTTPS hostname block forward the auth endpoints to localhost:7000. Without this Caddy would 404 from the static file server. Test surface: * tests/test_auth.py (new, 19 cases): hash roundtrip, users.json I/O, session create/lookup/expire/revoke. * tests/test_api.py: new admin_session fixture; existing HTTP tests updated to send the cookie; new tests cover login setup, login success, wrong-password 401, logout revocation, and the guard's 302/401 split. * tests/test_webinstaller_assets.py: new case that unpacks the users.json _write_file_cmd body and verifies the werkzeug hash round-trips against the step-1 password. Bumped version to 26.11-alpha and rolled CHANGELOG. Also folded in the ruff-format fix that was pending from 26.10-alpha's lint red. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:01:17 +02:00
*_post_install_commands(s["hostname"], s["username"], s["password"]),
],
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)