diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7f08a..cb9796d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,50 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ## [Unreleased] +### Security + +- **App names are validated everywhere.** `POST /api/apps/remove` with + `{"name": ".."}` or `"/"` sent `shutil.rmtree` at `/var/lib/furtka` or the + root filesystem (and wiping `users.json` reopened first-run admin setup to + the LAN). Names from URLs, JSON bodies and manifests must now match + `^[a-z][a-z0-9_-]*$`; `installer.remove`/`update_env` check it too. +- **Settings can't inject into `.env`.** Line breaks are refused; values + with `$` are single-quoted so compose doesn't expand them (`pa$word` used + to become `pa`). Validation runs before the write, so a rejected `/etc` + path is no longer saved anyway. +- **Damaged `users.json` fails closed.** An empty/corrupt file used to read + as "no admin yet" and turned `/login` into an open create-admin form. + `/login` now answers 503 with SSH recovery steps; `save_users` fsyncs. +- **API server hardening:** threaded (state changes still serialised), 1 MiB + body cap, 30 s socket timeout, cross-site POSTs refused via + `Sec-Fetch-Site`, JSON 500 instead of dropped connections. + ### Fixed +- **Installer disk picker:** SATA SSDs were labelled "HDD" and lost the + recommendation to bigger spinning disks; type now comes from lsblk + `TRAN`/`ROTA`. zram, eMMC boot/RPMB areas, read-only and 0-byte disks are + hidden; disks under 16 GB are greyed out; USB disks rank last; >2 TB disks + on BIOS machines get an MBR warning; the boot stick is also found via the + archiso kernel args when `bootmnt` is gone. Step 2 only accepts a listed disk. +- **Installer runs one install at a time** (double-click / second browser + started a second archinstall on the same disk), and a crashed archinstall + (OOM kill, signal) now shows as an error instead of a progress bar stuck + forever. Disk-probe exceptions land in the log instead of a bare 500. +- **Failed installs can be retried** from the error page without re-entering + everything. +- **No 2-minute boot stall on boards with a second NIC:** the target gets a + `systemd-networkd-wait-online --any` drop-in. +- **Self-update:** symlink flip is atomic; any error after the flip rolls + back (and restores the previous Caddyfile); health check survives read + timeouts; every failure ends in an `error` stage the settings page shows. +- **Interrupted app installs no longer block all later installs** — a stale + state with no live runner is marked failed after 60 s. +- **Catalog sync / update checks** no longer strand on socket resets or disk + full. +- **Apps page after an API restart** redirects to `/login` instead of + showing an empty app list. + - **Self-update starts newly added timers immediately and links units through `current`.** `_link_new_units()` enabled a timer a release introduced but never started it, so after the 26.19 → 26.20 update the @@ -19,6 +61,14 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r (installer-created links use `current`). Now: link via `current`, enable, start. +### Changed + +- Wizard wording: "Server name" with a live `http://.local` preview, + "Pick a disk" with an explicit erase warning, readable confirm table, + password hint explains it's also the SSH/console password, names are + lowercased instead of rejected, system account names are refused. +- Live console banner says a network cable with internet is required. + ## [26.20-alpha] - 2026-08-25 First release shaped by a real-hardware install (old laptop mainboard, BIOS boot, diff --git a/assets/www/index.html b/assets/www/index.html index 66e5e2c..86c1559 100644 --- a/assets/www/index.html +++ b/assets/www/index.html @@ -68,6 +68,15 @@ {% endblock %} diff --git a/webinstaller/templates/install/step2.html b/webinstaller/templates/install/step2.html index 0309e02..a82df57 100644 --- a/webinstaller/templates/install/step2.html +++ b/webinstaller/templates/install/step2.html @@ -1,39 +1,53 @@ {% extends "base.html" %} -{% block title %}Step 2 — Boot drive · Furtka Installer{% endblock %} +{% block title %}Step 2 — Disk · Furtka Installer{% endblock %} {% block step_indicator %}Step 2 of 3{% endblock %} {% block content %} -

Boot drive

-

Pick the disk Furtka will install onto. We pre-select the drive that looks fastest and healthiest.

+

Pick a disk

+

Choose the disk Furtka will be installed on. Everything on it will be erased. We pre-select the one that looks fastest and healthiest.

-{% if not drives %} -
- No physical disks detected. The live ISO can't see any install targets — check your BIOS / SATA controller. +{% set usable = drives | selectattr("usable") | list %} +{% set pick = selected if (selected and usable | selectattr("name", "equalto", selected) | list) else (usable[0].name if usable else "") %} + +{% if error %} + +{% endif %} + +{% if not usable %} + {% endif %}
{% for d in drives %} -
- + ← Back
diff --git a/website/tools/logo-assets.py b/website/tools/logo-assets.py index 0797638..66a98c7 100644 --- a/website/tools/logo-assets.py +++ b/website/tools/logo-assets.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# ruff: noqa: E501 — inline SVG markup reads better unwrapped """Generate the logo-poll showcase assets for /logo/ from one geometry source. python3 website/tools/logo-assets.py @@ -10,6 +11,7 @@ the output — the site build itself needs nothing beyond Hugo. Mark M: lowercase f knocked out of a gate, 64-unit grid (even-odd fill). Mark R: window, wheel and F, 104x114 grid, stroke 8 (from Robert's sketch). """ + import base64 import io import json @@ -119,7 +121,9 @@ def raster(svgtxt, w, h): def mark_png(k, tier, height, color=None): _, _, w, h = MARKS[k]["vb"] - return raster(svg(k, tier, color or MARKS[k]["color"], standalone=True), round(height * w / h), height) + return raster( + svg(k, tier, color or MARKS[k]["color"], standalone=True), round(height * w / h), height + ) # ---------------------------------------------------------------- wordmark @@ -141,7 +145,9 @@ def wordmark(text, size, x0=0.0, baseline=0.0, tracking=-0.028): g.draw(pen) d = pen.getCommands() if d: - parts.append(f'') + parts.append( + f'' + ) x += g.width * s + tracking * size return "".join(parts), x - x0 - tracking * size @@ -241,21 +247,24 @@ def vote_card(path): m = mark_png(k, "flat", 330) im.alpha_composite(m, (cx - m.width // 2, 110 - (m.height - 330) // 2)) size = 46 - d = ImageDraw.Draw(im) for text, cx, col in (("M", W // 4, "#6b6b6f"), ("R", 3 * W // 4, "#8a8a90")): paths, tw = wordmark(text, size) # letters via the same outlines: rasterise a tiny SVG for crisp text - svgtxt = (f'' - f'{paths}') + svgtxt = ( + f'' + f'{paths}' + ) t = raster(svgtxt, round(tw), round(size * 1.3)) im.alpha_composite(t, (cx - t.width // 2, 500)) # url line, centred on the seam paths, tw = wordmark("furtka.org/logo", 40) - svgtxt = (f'{paths}') + svgtxt = f'{paths}' t = raster(svgtxt, round(tw), 52) pad = 18 box = Image.new("RGBA", (t.width + 2 * pad, t.height + 2 * pad), "#f7f6f3") - ImageDraw.Draw(box).rounded_rectangle([0, 0, box.width - 1, box.height - 1], 14, outline="#e4e3dc") + ImageDraw.Draw(box).rounded_rectangle( + [0, 0, box.width - 1, box.height - 1], 14, outline="#e4e3dc" + ) box.alpha_composite(t, (pad, pad)) im.alpha_composite(box, (W // 2 - box.width // 2, 40)) im.convert("RGB").save(path, optimize=True)