- error state gets a 'Try again' button (settings are kept) plus links back to the disk / settings pages; the page reloads into it instead of only recolouring the bar - names are lowercased instead of rejected; system account names (root, caddy, docker, …) are refused with a clear message - plain wording: 'Server name' with a live http://<name>.local preview, 'Pick a disk' with an explicit erase warning, a readable confirm table (language label, disk size/type) and a Back button - the password hint says it is also the SSH / console password; the language hint says the wizard itself is English - wizard pages redirect to the progress page while an install is running - progress page: aria-live phase, aria-valuenow updates, 'lost connection' note, 'fine to close this page' - .local fallback on the reboot page: router device list, IP may change - live console banner says a network cable with internet is required - target gets a systemd-networkd-wait-online --any drop-in: boards with a second unplugged NIC (or Ethernet + Wi-Fi) waited up to 2 min at boot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
112 lines
5.1 KiB
HTML
112 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Installing… · Furtka Installer{% endblock %}
|
||
{% block head_extra %}
|
||
{# Fallback for users with JS disabled — otherwise the JS below takes over
|
||
and updates in-place so the log <details> doesn't re-collapse. #}
|
||
<noscript>
|
||
{% if progress.status == "running" %}<meta http-equiv="refresh" content="3">{% endif %}
|
||
</noscript>
|
||
{% endblock %}
|
||
{% block step_indicator %}<span class="step-indicator">Installing</span>{% endblock %}
|
||
|
||
{% block content %}
|
||
<h1 id="install-heading">
|
||
{% if progress.status == "done" %}Furtka is ready
|
||
{% elif progress.status == "error" %}Installation hit a snag
|
||
{% else %}Installing Furtka{% endif %}
|
||
</h1>
|
||
<p class="lede" id="install-lede">
|
||
{% if progress.status == "done" %}Installation finished. Two more steps and Furtka is running from its own disk.
|
||
{% elif progress.status == "error" %}{{ progress.phase }}.
|
||
{% else %}This takes 5–20 minutes, depending on your internet speed. Don't power off the machine — it's fine to close this page and come back.{% endif %}
|
||
</p>
|
||
|
||
{% if progress.status == "error" %}
|
||
<p>Your settings are kept, so you can simply try again. If it fails the same way twice,
|
||
open <em>Show details</em> below — the last lines of the log say what went wrong.</p>
|
||
<form method="post" action="{{ url_for('install_run') }}"
|
||
onsubmit="this.querySelector('button').disabled = true;">
|
||
<div class="actions">
|
||
<button type="submit" class="btn btn-primary">Try again</button>
|
||
<a href="{{ url_for('install_step_2') }}" class="btn btn-link">Pick a different disk</a>
|
||
<a href="{{ url_for('install_step_1') }}" class="btn btn-link">Change settings</a>
|
||
</div>
|
||
</form>
|
||
{% endif %}
|
||
|
||
{% if progress.status == "done" %}
|
||
{# The USB-stick step has to be read *before* the click: the machine reboots
|
||
3 s after it, and most BIOSes boot the stick before the disk — so the
|
||
installer would just come back. Seen on the first hardware bench. #}
|
||
<ol class="next-steps">
|
||
<li><strong>Pull out the USB stick</strong> (or eject the installer ISO in your VM).
|
||
If it stays in, the computer will start this installer again instead of Furtka.</li>
|
||
<li>Click the button below. The computer restarts and comes back as
|
||
<strong>http://{{ hostname }}.local</strong> in about a minute.</li>
|
||
</ol>
|
||
<form method="post" action="{{ url_for('install_reboot') }}"
|
||
onsubmit="return confirm('USB stick removed? Click OK to restart the computer.');">
|
||
<div class="actions">
|
||
<button type="submit" class="btn btn-primary">USB stick is out — restart now</button>
|
||
</div>
|
||
</form>
|
||
{% endif %}
|
||
|
||
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ progress.percent }}">
|
||
<div id="progress-bar" 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" aria-live="polite"><span id="progress-phase">{{ progress.phase }}</span> · <span id="progress-percent">{{ progress.percent }}</span>%</p>
|
||
|
||
<details class="log-details">
|
||
<summary>Show details</summary>
|
||
<pre id="install-log" class="log">{{ log or "(waiting for install to start)" }}</pre>
|
||
</details>
|
||
|
||
<script>
|
||
(function () {
|
||
var initialStatus = {{ progress.status | tojson }};
|
||
if (initialStatus !== 'running') return;
|
||
|
||
var bar = document.getElementById('progress-bar');
|
||
var phaseEl = document.getElementById('progress-phase');
|
||
var percentEl = document.getElementById('progress-percent');
|
||
var logEl = document.getElementById('install-log');
|
||
var failures = 0;
|
||
|
||
function tick() {
|
||
fetch('{{ url_for("install_log_json") }}', { cache: 'no-store' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
var p = data.progress;
|
||
bar.style.width = p.percent + '%';
|
||
bar.parentNode.setAttribute('aria-valuenow', p.percent);
|
||
phaseEl.textContent = p.phase;
|
||
percentEl.textContent = p.percent;
|
||
if (logEl.textContent !== data.log) {
|
||
var atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8;
|
||
logEl.textContent = data.log || '(waiting for install to start)';
|
||
if (atBottom) logEl.scrollTop = logEl.scrollHeight;
|
||
}
|
||
if (p.status === 'done' || p.status === 'error') {
|
||
// Reload so the server-rendered Done / error state (with
|
||
// its Reboot or Try-again buttons) replaces the
|
||
// running-state markup.
|
||
window.location.reload();
|
||
return;
|
||
}
|
||
failures = 0;
|
||
setTimeout(tick, 3000);
|
||
})
|
||
.catch(function () {
|
||
failures += 1;
|
||
if (failures >= 2) {
|
||
phaseEl.textContent = 'Lost connection to the installer — retrying…';
|
||
}
|
||
setTimeout(tick, 3000);
|
||
});
|
||
}
|
||
setTimeout(tick, 3000);
|
||
})();
|
||
</script>
|
||
{% endblock %}
|