2026-04-14 19:51:50 +02:00
|
|
|
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
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
import sys
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-04-13 19:44:29 +02:00
|
|
|
from drives import list_scored_devices
|
2026-04-14 18:08:59 +02:00
|
|
|
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
2026-04-13 19:38:34 +02:00
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
LANGUAGES = {
|
|
|
|
|
"en": {"locale": "en_US.UTF-8", "label": "English"},
|
|
|
|
|
"de": {"locale": "de_DE.UTF-8", "label": "Deutsch"},
|
|
|
|
|
"pl": {"locale": "pl_PL.UTF-8", "label": "Polski"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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": "",
|
2026-04-13 19:44:29 +02:00
|
|
|
"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}$")
|
|
|
|
|
|
2026-04-14 17:07:57 +02:00
|
|
|
# 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 = [
|
2026-04-14 18:29:42 +02:00
|
|
|
("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!"),
|
2026-04-14 17:07:57 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 17:00:39 +02:00
|
|
|
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,
|
|
|
|
|
filesystem_type=FilesystemType.Ext4,
|
|
|
|
|
separate_home=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
layout = DiskLayoutConfiguration(
|
|
|
|
|
config_type=DiskLayoutType.Default,
|
|
|
|
|
device_modifications=[device_mod],
|
|
|
|
|
)
|
|
|
|
|
return layout.json()
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 19:51:50 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 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.
|
|
|
|
|
#
|
|
|
|
|
# Files are shipped inline (base64-encoded) rather than copied from the live
|
|
|
|
|
# ISO because archinstall's chroot can't see the live filesystem. Payload is
|
|
|
|
|
# small (~200 lines across 9 files) so this is cheaper than a tarball dance.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
_CADDYFILE = """\
|
|
|
|
|
# Serves the Furtka landing page + status.json on :80. Static only for now;
|
|
|
|
|
# reverse_proxy / TLS / auth come later when Authentik is wired in.
|
|
|
|
|
:80 {
|
|
|
|
|
\troot * /srv/furtka/www
|
|
|
|
|
\tfile_server
|
|
|
|
|
\tencode gzip
|
|
|
|
|
\tlog {
|
|
|
|
|
\t\toutput stdout
|
|
|
|
|
\t}
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_INDEX_HTML = """\
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<title>Furtka</title>
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
|
<link rel="stylesheet" href="/style.css">
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<main>
|
|
|
|
|
<header>
|
|
|
|
|
<h1>Welcome to Furtka</h1>
|
|
|
|
|
<p class="lead">Your home server is ready.</p>
|
|
|
|
|
<p class="host">Running on <code>__HOSTNAME__</code></p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<section class="status">
|
|
|
|
|
<h2>System status</h2>
|
|
|
|
|
<div class="tiles">
|
|
|
|
|
<div class="tile">
|
|
|
|
|
<span class="label">Uptime</span>
|
|
|
|
|
<span class="value" id="uptime">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tile">
|
|
|
|
|
<span class="label">Docker</span>
|
|
|
|
|
<span class="value" id="docker">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tile">
|
|
|
|
|
<span class="label">Free disk</span>
|
|
|
|
|
<span class="value" id="disk">—</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="updated">Updated <span id="updated">—</span></p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="soon">
|
|
|
|
|
<h2>App store</h2>
|
|
|
|
|
<p>Coming soon — one-click installs for Nextcloud, Jellyfin, and friends.</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<footer>
|
|
|
|
|
<p>Furtka · <a href="https://furtka.org">furtka.org</a></p>
|
|
|
|
|
</footer>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
async function refresh() {
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch('/status.json', {cache: 'no-store'});
|
|
|
|
|
if (!r.ok) return;
|
|
|
|
|
const s = await r.json();
|
|
|
|
|
document.getElementById('uptime').textContent = s.uptime || '—';
|
|
|
|
|
document.getElementById('docker').textContent = s.docker_version || '—';
|
|
|
|
|
document.getElementById('disk').textContent = s.disk_free || '—';
|
|
|
|
|
document.getElementById('updated').textContent = s.updated_at || '—';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
/* next tick will retry */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
refresh();
|
|
|
|
|
setInterval(refresh, 15000);
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_STYLE_CSS = """\
|
|
|
|
|
:root {
|
|
|
|
|
--bg: #0f1115;
|
|
|
|
|
--fg: #e8eaed;
|
|
|
|
|
--muted: #9aa0a6;
|
|
|
|
|
--accent: #6ee7b7;
|
|
|
|
|
--card: #1a1d24;
|
|
|
|
|
}
|
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
color: var(--fg);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
main { max-width: 780px; margin: 0 auto; padding: 4rem 1.5rem; }
|
|
|
|
|
header h1 { margin: 0 0 0.5rem; font-size: 2.5rem; }
|
|
|
|
|
.lead { font-size: 1.25rem; color: var(--muted); margin: 0 0 0.25rem; }
|
|
|
|
|
.host { color: var(--muted); margin: 0 0 3rem; }
|
|
|
|
|
.host code {
|
|
|
|
|
background: var(--card);
|
|
|
|
|
padding: 0.15rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
}
|
|
|
|
|
section h2 {
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.1em;
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
margin: 2rem 0 1rem;
|
|
|
|
|
}
|
|
|
|
|
.tiles {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.tile {
|
|
|
|
|
background: var(--card);
|
|
|
|
|
padding: 1.25rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.tile .label {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
}
|
|
|
|
|
.tile .value { font-size: 1.25rem; margin-top: 0.5rem; }
|
|
|
|
|
.updated { font-size: 0.85rem; color: var(--muted); margin-top: 1rem; }
|
|
|
|
|
.soon {
|
|
|
|
|
background: var(--card);
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-top: 2rem;
|
|
|
|
|
}
|
|
|
|
|
footer {
|
|
|
|
|
margin-top: 4rem;
|
|
|
|
|
padding-top: 1.5rem;
|
|
|
|
|
border-top: 1px solid #2a2e36;
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
footer a { color: var(--accent); }
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_STATUS_JSON_PLACEHOLDER = """\
|
|
|
|
|
{
|
|
|
|
|
"hostname": "",
|
|
|
|
|
"uptime": "starting…",
|
|
|
|
|
"docker_version": "starting…",
|
|
|
|
|
"disk_free": "starting…",
|
|
|
|
|
"updated_at": ""
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_STATUS_SH = """\
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
# Writes /srv/furtka/www/status.json with current system stats. Fired by
|
|
|
|
|
# furtka-status.timer every 30s; also runs once 10s after boot.
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
out=/srv/furtka/www/status.json
|
|
|
|
|
tmp=$(mktemp)
|
|
|
|
|
|
|
|
|
|
hostname=$(cat /etc/hostname)
|
|
|
|
|
uptime=$(uptime -p 2>/dev/null | sed 's/^up //' || echo unknown)
|
|
|
|
|
if command -v docker >/dev/null 2>&1; then
|
|
|
|
|
docker_version=$(docker --version 2>/dev/null \
|
|
|
|
|
| awk '{print $3}' | tr -d ',' || echo unavailable)
|
|
|
|
|
else
|
|
|
|
|
docker_version=unavailable
|
|
|
|
|
fi
|
|
|
|
|
disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4 " free of " $2}' || echo unknown)
|
|
|
|
|
updated_at=$(date -Iseconds)
|
|
|
|
|
|
|
|
|
|
cat > "$tmp" <<EOF
|
|
|
|
|
{
|
|
|
|
|
"hostname": "$hostname",
|
|
|
|
|
"uptime": "$uptime",
|
|
|
|
|
"docker_version": "$docker_version",
|
|
|
|
|
"disk_free": "$disk_free",
|
|
|
|
|
"updated_at": "$updated_at"
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
mv "$tmp" "$out"
|
|
|
|
|
chmod 644 "$out"
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_STATUS_SERVICE = """\
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=Refresh Furtka system status JSON
|
|
|
|
|
After=network-online.target
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=oneshot
|
|
|
|
|
ExecStart=/usr/local/bin/furtka-status
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_STATUS_TIMER = """\
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=Refresh Furtka system status every 30s
|
|
|
|
|
|
|
|
|
|
[Timer]
|
|
|
|
|
OnBootSec=10s
|
|
|
|
|
OnUnitActiveSec=30s
|
|
|
|
|
AccuracySec=5s
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=timers.target
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_WELCOME_SH = """\
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
# Regenerates /etc/issue on the installed system so the console tells the
|
|
|
|
|
# user which URL to open. Mirrors the live-ISO furtka-update-issue pattern.
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
hostname=$(cat /etc/hostname)
|
|
|
|
|
ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
echo
|
|
|
|
|
echo " Furtka is ready."
|
|
|
|
|
echo
|
|
|
|
|
echo " Open in a browser on another device on your network:"
|
|
|
|
|
echo
|
|
|
|
|
echo " http://${hostname}.local (easy — try this first)"
|
|
|
|
|
if [ -n "$ip" ]; then
|
|
|
|
|
echo " http://${ip} (fallback if the first doesn't work)"
|
|
|
|
|
fi
|
|
|
|
|
echo
|
|
|
|
|
} > /etc/issue
|
|
|
|
|
|
|
|
|
|
agetty --reload 2>/dev/null || true
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_WELCOME_SERVICE = """\
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=Furtka console welcome banner
|
|
|
|
|
After=network-online.target
|
|
|
|
|
Wants=network-online.target
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=oneshot
|
|
|
|
|
ExecStart=/usr/local/bin/furtka-welcome
|
|
|
|
|
RemainAfterExit=yes
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
"""
|
|
|
|
|
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
# Tarball built by iso/build.sh containing the furtka/ Python package + the
|
|
|
|
|
# bundled apps/ tree. The webinstaller reads it from the 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")
|
|
|
|
|
|
|
|
|
|
_FURTKA_WRAPPER_SH = """\
|
|
|
|
|
#!/bin/sh
|
|
|
|
|
# Tiny launcher for the furtka resource-manager CLI. The Python source lives
|
|
|
|
|
# under /opt/furtka/furtka/ — added to PYTHONPATH so plain `python3 -m` finds
|
|
|
|
|
# it without needing pip on the target system.
|
|
|
|
|
PYTHONPATH=/opt/furtka exec python3 -m furtka.cli "$@"
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_FURTKA_RECONCILE_SERVICE = """\
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=Furtka app reconciler (boot-scan)
|
|
|
|
|
Requires=docker.service
|
|
|
|
|
After=docker.service network-online.target
|
|
|
|
|
Wants=network-online.target
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=oneshot
|
|
|
|
|
ExecStart=/usr/local/bin/furtka reconcile
|
|
|
|
|
RemainAfterExit=no
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
"""
|
|
|
|
|
|
2026-04-14 19:51:50 +02:00
|
|
|
|
|
|
|
|
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): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
def _resource_manager_commands():
|
|
|
|
|
"""Commands to land /opt/furtka/ + the `furtka` CLI + reconcile.service.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
of the install still works — the resource manager just won't be installed.
|
|
|
|
|
"""
|
|
|
|
|
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()
|
|
|
|
|
untar_cmd = (
|
|
|
|
|
f"mkdir -p /opt/furtka && printf %s {payload_b64} | base64 -d | tar -xzf - -C /opt/furtka"
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
untar_cmd,
|
|
|
|
|
_write_file_cmd("/usr/local/bin/furtka", _FURTKA_WRAPPER_SH, mode="755"),
|
|
|
|
|
_write_file_cmd("/etc/systemd/system/furtka-reconcile.service", _FURTKA_RECONCILE_SERVICE),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 19:51:50 +02:00
|
|
|
def _post_install_commands(hostname):
|
|
|
|
|
# nss-mdns: splice `mdns_minimal [NOTFOUND=return]` before `resolve` on
|
|
|
|
|
# the hosts line so `*.local` works from the installed system too. Guarded
|
|
|
|
|
# so a re-run (or a future Arch default that already ships mdns) is a
|
|
|
|
|
# no-op instead of double-injecting.
|
|
|
|
|
nss_sed = (
|
|
|
|
|
"grep -q 'mdns_minimal' /etc/nsswitch.conf || "
|
|
|
|
|
"sed -i '/^hosts:/ s/resolve/mdns_minimal [NOTFOUND=return] resolve/' "
|
|
|
|
|
"/etc/nsswitch.conf"
|
|
|
|
|
)
|
|
|
|
|
# Pin the chosen hostname into the static HTML at install-time so the
|
|
|
|
|
# landing page doesn't need a server-side template.
|
2026-04-15 08:46:58 +02:00
|
|
|
hostname_sed = f"sed -i 's/__HOSTNAME__/{hostname}/g' /srv/furtka/www/index.html"
|
2026-04-14 19:51:50 +02:00
|
|
|
return [
|
|
|
|
|
_write_file_cmd("/etc/caddy/Caddyfile", _CADDYFILE),
|
|
|
|
|
_write_file_cmd("/srv/furtka/www/index.html", _INDEX_HTML),
|
|
|
|
|
_write_file_cmd("/srv/furtka/www/style.css", _STYLE_CSS),
|
|
|
|
|
_write_file_cmd("/srv/furtka/www/status.json", _STATUS_JSON_PLACEHOLDER),
|
|
|
|
|
_write_file_cmd("/usr/local/bin/furtka-status", _FURTKA_STATUS_SH, mode="755"),
|
|
|
|
|
_write_file_cmd("/usr/local/bin/furtka-welcome", _FURTKA_WELCOME_SH, mode="755"),
|
2026-04-15 08:46:58 +02:00
|
|
|
_write_file_cmd("/etc/systemd/system/furtka-status.service", _FURTKA_STATUS_SERVICE),
|
|
|
|
|
_write_file_cmd("/etc/systemd/system/furtka-status.timer", _FURTKA_STATUS_TIMER),
|
|
|
|
|
_write_file_cmd("/etc/systemd/system/furtka-welcome.service", _FURTKA_WELCOME_SERVICE),
|
2026-04-14 19:51:50 +02:00
|
|
|
nss_sed,
|
|
|
|
|
hostname_sed,
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
*_resource_manager_commands(),
|
2026-04-14 20:34:34 +02:00
|
|
|
# archinstall calls `systemctl enable` on `services` *before*
|
|
|
|
|
# custom_commands runs, so our own unit files aren't on disk yet at
|
|
|
|
|
# that point. Enable them here, after they exist. caddy /
|
|
|
|
|
# avahi-daemon stay in the `services` list — those are packaged
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
# units, present right after pacstrap. furtka-reconcile is enabled
|
|
|
|
|
# only if the resource manager payload was actually installed above.
|
|
|
|
|
"systemctl enable furtka-welcome.service furtka-status.timer "
|
|
|
|
|
"$([ -e /etc/systemd/system/furtka-reconcile.service ] "
|
|
|
|
|
"&& echo furtka-reconcile.service)",
|
2026-04-14 19:51:50 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-04-15 09:11:58 +02:00
|
|
|
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,
|
2026-04-15 09:11:58 +02:00
|
|
|
"bootloader": _detect_bootloader(),
|
2026-04-14 17:00:39 +02:00
|
|
|
"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"],
|
2026-04-14 19:51:50 +02:00
|
|
|
"packages": [
|
|
|
|
|
"docker",
|
|
|
|
|
"docker-compose",
|
|
|
|
|
"vim",
|
|
|
|
|
"git",
|
|
|
|
|
"htop",
|
|
|
|
|
"curl",
|
|
|
|
|
# Base OS post-install (landing page + mDNS on installed system).
|
|
|
|
|
"caddy",
|
|
|
|
|
"avahi",
|
|
|
|
|
"nss-mdns",
|
feat(furtka): ship resource manager + fileshare app on the ISO — slice 3
Closes the loop end-to-end. The ISO build now bundles the furtka/
package and the apps/ tree as a tarball; webinstaller hands it to
archinstall via custom_commands; the installed system gets the
`furtka` CLI, a boot-scan systemd unit, and the fileshare app
ready to install.
- iso/build.sh: stages furtka/ + apps/ into a tmpdir, drops
__pycache__, tarballs into airootfs/opt/furtka-resource-manager.tar.gz.
- webinstaller/app.py: _resource_manager_commands() reads the staged
payload at request-time, base64-encodes it into a single untar
command, and writes /usr/local/bin/furtka (PYTHONPATH wrapper, no
pip needed) + furtka-reconcile.service. Python pacstrapped so the
wrapper has an interpreter.
- Graceful degradation: dev box / CI without an ISO build has no
payload tarball, so those commands are skipped (logs a warning).
Tests cover both branches.
- furtka-reconcile.service is conditionally enabled only if the unit
file actually landed — keeps the systemctl enable line green when
the payload was absent.
- apps/fileshare/: first real Furtka app. dperson/samba on host
network, single named volume, .env.example with placeholder creds.
Manifest matches the schema locked in slice 1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:06:01 +02:00
|
|
|
# Resource manager runtime — pure-stdlib Python, no pip needed
|
|
|
|
|
# because we expose the package via PYTHONPATH in /usr/local/bin/furtka.
|
|
|
|
|
"python",
|
2026-04-14 19:51:50 +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
|
|
|
"profile": {"type": "server"},
|
2026-04-14 19:51:50 +02:00
|
|
|
"services": [
|
|
|
|
|
"docker",
|
2026-04-14 20:34:34 +02:00
|
|
|
# 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.
|
2026-04-14 19:51:50 +02:00
|
|
|
"caddy",
|
|
|
|
|
"avahi-daemon",
|
|
|
|
|
],
|
|
|
|
|
# `gpasswd -a <user> docker` has to stay first — adds the user to
|
|
|
|
|
# the docker group once the group exists (archinstall creates users
|
|
|
|
|
# before pacstrapping extras). After that we drop the Furtka landing
|
|
|
|
|
# page, status timer, and welcome banner into place.
|
|
|
|
|
"custom_commands": [
|
|
|
|
|
f"gpasswd -a {s['username']} docker",
|
|
|
|
|
*_post_install_commands(s["hostname"]),
|
|
|
|
|
],
|
feat: webinstaller writes archinstall config + execs install, styled
Wires the live-ISO wizard from "shows three screens" to "actually invokes
archinstall on the chosen disk", plus first-pass styling so it stops looking
like raw <h1>/<form>.
Webinstaller flow:
- S1 form gains username/password/password2/language with server-side
validation (hostname/username regex, ≥8 char password, match check).
- /install/run writes user_configuration.json + user_credentials.json
(creds 0600) to FURTKA_STATE_DIR (default /tmp/furtka), then execs
`archinstall --config … --creds … --silent` as a backgrounded subprocess.
- /install/log renders the subprocess output via meta-refresh polling.
- FURTKA_DRY_RUN=1 short-circuits the exec for testing.
- archinstall flag names verified against `archinstall --help` in an
archlinux container before committing.
Drive list:
- drives.py now filters via `lsblk … -o NAME,SIZE,TYPE` keeping TYPE=disk,
so the live ISO's own squashfs (loop) and CD-ROM (rom) stop appearing
as install targets.
Boot menu:
- iso/build.sh sed-rebrands "Arch Linux install medium" →
"Furtka Live Installer" across grub/, syslinux/, and efiboot/loader/
entries. Verified zero leftovers against the current releng profile.
Styling:
- static/style.css adopts the website's design tokens (palette,
typography, gate-mark accent), with light + dark via prefers-color-scheme.
- New base.html with header (gate SVG + FURTKA·INSTALLER wordmark + step
indicator) and footer; all install templates extend it.
- Drive picker uses radio cards with score chip; overview uses a summary
table and a destructive "wipe drive" button.
Tests: 17 pass (4 new in test_app.py covering validation + config builders,
2 new in test_drives.py covering the lsblk filter). Ruff clean.
README roadmap updated to mark these done and explicitly defer the
26.0-alpha release until archinstall actually completes end-to-end on a VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:54:49 +02:00
|
|
|
"network_config": {"type": "iso"},
|
|
|
|
|
"ssh": True,
|
|
|
|
|
"audio_config": None,
|
|
|
|
|
"locale_config": {
|
|
|
|
|
"locale": LANGUAGES[s["language"]]["locale"],
|
|
|
|
|
"keyboard_layout": "us",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_archinstall_creds(s):
|
2026-04-14 17:00:39 +02:00
|
|
|
# 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 {
|
2026-04-14 17:00:39 +02:00
|
|
|
"!root-password": s["password"],
|
|
|
|
|
"users": [
|
|
|
|
|
{
|
|
|
|
|
"username": s["username"],
|
|
|
|
|
"!password": s["password"],
|
|
|
|
|
"sudo": True,
|
2026-04-14 17:07:57 +02:00
|
|
|
"groups": [],
|
2026-04-14 17:00:39 +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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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",
|
2026-04-14 18:29:42 +02:00
|
|
|
"--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:44:29 +02:00
|
|
|
|
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:44:29 +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():
|
2026-04-14 17:07:57 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2026-04-14 18:08:59 +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"))
|
2026-04-15 09:24:05 +02:00
|
|
|
# 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).
|
2026-04-14 18:08:59 +02:00
|
|
|
subprocess.Popen(
|
2026-04-15 09:24:05 +02:00
|
|
|
["/bin/sh", "-c", "sleep 3 && /usr/bin/systemctl reboot"],
|
2026-04-14 18:08:59 +02:00
|
|
|
start_new_session=True,
|
|
|
|
|
)
|
2026-04-15 09:27:49 +02:00
|
|
|
return render_template("install/rebooting.html", hostname=settings["hostname"])
|
2026-04-14 18:08:59 +02:00
|
|
|
|
|
|
|
|
|
2026-04-13 19:38:34 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
app.run(debug=True, port=5000)
|