feat: install progress bar + fix docker group creation order
Two tangled changes to the install flow, batched because they're both
small and hit app.py:
1. Phase-based progress bar on /install/log. parse_install_progress()
scans the archinstall log for ordered phase markers ("Wiping
partitions", "Installing packages: ['base'", "Adding bootloader",
"Installation completed without any errors", …) and exposes
percent + user-facing phase label + status (running/done/error).
Template wraps the raw log in a collapsed <details> so the default
view stays calm; the meta-refresh stops once status is terminal.
If archinstall changes its stdout wording the bar stalls on the
last recognized phase — the install itself is unaffected.
2. Drop "docker" from the user's groups in creds and do the
`gpasswd -a <user> docker` via custom_commands instead.
archinstall creates users before pacstrapping the extras list, so
the docker group doesn't exist at user-create time —
caused the second real install to crash with
`gpasswd: group 'docker' does not exist`. custom_commands runs
at the very end, after docker is installed. Username is validated
by USERNAME_RE so no shell injection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
51cdf460d9
commit
3a259beb98
3 changed files with 112 additions and 6 deletions
|
|
@ -33,6 +33,46 @@ settings = {
|
|||
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}
|
||||
|
||||
|
||||
def validate_step1(form):
|
||||
errors = []
|
||||
|
|
@ -102,6 +142,11 @@ def build_archinstall_config(s):
|
|||
"packages": ["docker", "docker-compose", "vim", "git", "htop", "curl"],
|
||||
"profile": {"type": "server"},
|
||||
"services": ["docker"],
|
||||
# Add user to the docker group post-install. We can't put "docker" in
|
||||
# the user's `groups` at create-time because archinstall creates users
|
||||
# before pacstrapping the extras, so the docker group doesn't exist
|
||||
# yet. custom_commands runs at the very end.
|
||||
"custom_commands": [f"gpasswd -a {s['username']} docker"],
|
||||
"network_config": {"type": "iso"},
|
||||
"ssh": True,
|
||||
"audio_config": None,
|
||||
|
|
@ -123,7 +168,7 @@ def build_archinstall_creds(s):
|
|||
"username": s["username"],
|
||||
"!password": s["password"],
|
||||
"sudo": True,
|
||||
"groups": ["docker"],
|
||||
"groups": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -216,8 +261,12 @@ def install_run():
|
|||
|
||||
@app.route("/install/log")
|
||||
def install_log_view():
|
||||
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else "(waiting for install to start)\n"
|
||||
return render_template("install/log.html", log=log)
|
||||
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else ""
|
||||
return render_template(
|
||||
"install/log.html",
|
||||
log=log,
|
||||
progress=parse_install_progress(log),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -337,6 +337,45 @@ select:focus {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 0 0.5rem;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--accent),
|
||||
color-mix(in srgb, var(--accent) 55%, #fff)
|
||||
);
|
||||
transition: width 0.6s ease;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.progress-bar-error { background: var(--danger); }
|
||||
.progress-bar-done { background: var(--success); }
|
||||
.progress-phase {
|
||||
margin: 0.3rem 0 1.5rem;
|
||||
color: var(--fg-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.log-details {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.log-details summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
color: var(--fg-muted);
|
||||
user-select: none;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.log-details summary:hover { color: var(--fg); }
|
||||
.log-details[open] summary { margin-bottom: 0.6rem; }
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────────── */
|
||||
|
||||
.site-footer {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Installing… · Furtka Installer{% endblock %}
|
||||
{% block head_extra %}<meta http-equiv="refresh" content="3">{% endblock %}
|
||||
{% block head_extra %}
|
||||
{% if progress.status == "running" %}<meta http-equiv="refresh" content="3">{% endif %}
|
||||
{% endblock %}
|
||||
{% block step_indicator %}<span class="step-indicator">Installing</span>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if progress.status == "done" %}
|
||||
<h1>Furtka is ready</h1>
|
||||
<p class="lede">Installation finished. Remove the USB / eject the installer image, then reboot.</p>
|
||||
{% elif progress.status == "error" %}
|
||||
<h1>Installation hit a snag</h1>
|
||||
<p class="lede">Something went wrong. Open the details below and share them so we can help.</p>
|
||||
{% else %}
|
||||
<h1>Installing Furtka</h1>
|
||||
<p class="lede">This page reloads every 3 seconds. Don't close it. Don't power off.</p>
|
||||
<p class="lede">This takes a few minutes. Don't close this page or power off the machine.</p>
|
||||
{% endif %}
|
||||
|
||||
<pre class="log">{{ log }}</pre>
|
||||
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ progress.percent }}">
|
||||
<div class="progress-bar{% if progress.status == 'error' %} progress-bar-error{% endif %}{% if progress.status == 'done' %} progress-bar-done{% endif %}" style="width: {{ progress.percent }}%;"></div>
|
||||
</div>
|
||||
<p class="progress-phase">{{ progress.phase }} · {{ progress.percent }}%</p>
|
||||
|
||||
<details class="log-details">
|
||||
<summary>Show details</summary>
|
||||
<pre class="log">{{ log or "(waiting for install to start)" }}</pre>
|
||||
</details>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue