# Changelog All notable changes to Furtka will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, release 0, alpha stage). ## [Unreleased] ## [26.15-alpha] - 2026-04-21 ### Fixed - **HTTPS is now opt-in; fresh installs no longer hit unbypassable SEC_ERROR_BAD_SIGNATURE.** Every version since 26.5 shipped a Caddyfile with a `__FURTKA_HOSTNAME__.local { tls internal }` site block, so Caddy auto-generated a self-signed root CA + intermediate + leaf on first boot. That worked for first-time-ever users, but every reinstall (or second Furtka box on the same LAN) produced a new CA with the **same intermediate CN** (`Caddy Local Authority - ECC Intermediate` — Caddy hardcodes it). Any browser that had ever trusted an earlier Furtka CA got a cached intermediate with mismatched keys, then Firefox's cert lookup substituted the cached intermediate when validating the new box's leaf → the signature check failed → `SEC_ERROR_BAD_SIGNATURE`, which Firefox has no "Advanced → Accept Risk" bypass for. - Removed the hostname site block from the default Caddyfile. Fresh installs serve `:80` only; visiting `https://furtka.local` now yields a clean connection-refused instead of the crypto fault. - Added top-level `import /etc/caddy/furtka-https.d/*.caddyfile`. The `/settings` HTTPS toggle (via `furtka.https.set_force_https`) now writes TWO snippets atomically — the top-level hostname + `tls internal` block (enables `:443`) and the `:80`-scoped redirect (forces HTTP → HTTPS) — and removes both on disable. Caddy reloads after the pair-swap; failure rolls both back. - Webinstaller creates `/etc/caddy/furtka-https.d/` during post-install alongside the existing `furtka.d/`. - `updater._refresh_caddyfile` runs a 26.14 → 26.15 migration: if the box already had the redirect snippet on disk (user had explicitly enabled "Force HTTPS" under the old regime), the migration also writes the new listener snippet so HTTPS keeps working across the upgrade. - **`status.force_https` now reads the listener snippet, not the redirect snippet.** A lone redirect without a `:443` listener wouldn't actually serve HTTPS, so the listener file is the authoritative "HTTPS is on" signal. The UI on `/settings` sees the correct state as a result. Known remaining UX wart: a browser that trusted a previous Furtka box still sees `BAD_SIGNATURE` when visiting this box's `https://` after enabling HTTPS here — the fixed intermediate CN is a Caddy-side limitation we can't fix from Furtka. Fresh installs on a browser that never visited another Furtka box work correctly. Workaround: `about:networking#sts` → Forget → clear `cert9.db`. ## [26.14-alpha] - 2026-04-21 ### Fixed - **Landing page and `/settings/` were silently bypassing the auth guard.** Since 26.11 shipped login, the Caddyfile only reverse-proxied `/api/*`, `/apps*`, `/login*`, and `/logout*` to Python. Everything else — including `/` and `/settings/` — fell through to Caddy's catch-all `file_server` and was served straight from `assets/www/` without ever hitting the session check. The effect: a LAN visitor saw the box's hostname, IP, Furtka version, and the buttons for Update-now / Reboot / HTTPS-toggle. The API calls those buttons fired were all 401-auth-gated so actions didn't land, but the information leak and the "looks open" UX was a real bug. Caught in the 26.13 SSH test session when the user noticed Logout only showed up on `/apps`. Now Caddy routes `/` and `/settings*` through Python; a new `_serve_static_www` handler checks the session cookie, redirects to `/login` if unauthed, and reads the HTML from `assets/www/` otherwise. Catch-all still serves `/style.css`, `/rootCA.crt`, and the runtime JSON files publicly — those don't need auth. - **Logout link now shows on every authed page, not just `/apps`.** The static HTML for `/` and `/settings/` maintained their own nav separate from `_HTML` in `api.py`, so they never got the Logout entry when it was added in 26.11. Both nav bars now include it plus an inline `doLogout()` that POSTs `/logout` and bounces to `/login`, matching the pattern in `_HTML`. ## [26.13-alpha] - 2026-04-21 ### Fixed - **Upgrade path from pre-auth releases actually works.** 26.11-alpha introduced `from werkzeug.security import ...` in `furtka/auth.py`, but werkzeug isn't installed on the target system — core runs as system Python with stdlib only, and `flask>=3.0` in `pyproject.toml` is never pip-installed on the box. Fresh boxes from the 26.11/26.12 ISO without a manually-installed werkzeug crashed on import; boxes upgrading from pre-26.11 got double-broken by that plus the health check below. Replaced the werkzeug dependency with a stdlib-only `furtka/passwd.py` that uses `hashlib.pbkdf2_hmac` for new hashes and parses werkzeug's `scrypt:N:r:p$salt$hex` format for backward compatibility — existing `users.json` files created on the rare boxes that did have werkzeug keep working after this upgrade, no re-setup needed. `from werkzeug.security import ...` is gone from the import chain entirely; `pyproject.toml`'s flask dep stays only for the live-ISO webinstaller. - **Self-update no longer auto-rolls-back when crossing the auth boundary.** `updater._health_check` pinged `/api/apps` and demanded a 200, which meant every 26.10 → 26.11+ upgrade hit the post-restart check, got a 401 (auth guard), and treated that as "server dead" → rollback. Now any 2xx–4xx response counts as "server alive"; only connection-level failures or 5xx fail the check. 5xx still fails rollback because that means the new process is up but broken. - **Install lock closes its race window.** `POST /api/apps/install` used to release the fcntl lock immediately after the sync pre-validation so the systemd-run child could re-acquire it — leaving a tiny gap where a second POST could slip in, pass the lock check, and return 202. Both child processes would start, one would win the in-child lock, the other would die silently. Now the API also reads `install-state.json` and refuses with 409 if the stage is non-terminal (`pulling_image`, `creating_volumes`, `starting_container`). The fcntl lock stays as belt-and-suspenders. ## [26.12-alpha] - 2026-04-21 ### Changed - **App-Install geht async mit Live-Progress.** `POST /api/apps/install` returnt jetzt `202 Accepted` nach der synchronen Pre-Validation (Source auflösen, Files kopieren, `.env` schreiben, Placeholder- und Path-Checks). Den eigentlichen Docker-Teil (`compose pull` → volumes → `compose up`) dispatched der Handler als `systemd-run --unit=furtka-install-` Hintergrund-Job, der seine Phase in `/var/lib/furtka/install-state.json` schreibt. Neues `GET /api/apps/install/status` für UI-Polling. Das Install-Modal zeigt jetzt live "Image wird heruntergeladen…" → "Speicherbereiche werden erstellt…" → "Container wird gestartet…" statt ~30 Sekunden totem "Installing…". Muster 1:1 parallel zu `/api/catalog/sync/apply` und `/api/furtka/update/apply`. Neue CLI- Subcommand `furtka app install-bg ` (intern, von der API aufgerufen); `furtka app install` für Terminal-User bleibt synchron. Die Reinstall-Taste in der App-Liste pollt ebenfalls den Install-Status und spiegelt die Phase im Button-Text. ## [26.11-alpha] - 2026-04-21 ### Added - **Login-auth for the Furtka web UI.** Every `/apps`, `/api/*`, `/`, and `/settings/` route now requires a signed-in session. New `/login` page serves a username/password form; `POST /login` validates against `/var/lib/furtka/users.json` (werkzeug PBKDF2- hashed), sets a `furtka_session` cookie (`HttpOnly`, `SameSite= Strict`, 7-day TTL), and redirects to `/apps`. `POST /logout` revokes the server-side session and clears the cookie. Unauthenticated HTML requests get a 302 to `/login`; unauthenticated API requests get 401 JSON. The old "No authentication on this UI yet" banner is gone; the `/apps` header picks up a `Logout` link instead. - **First-run setup fallback for upgrade-path boxes.** Boxes upgrading from 26.10-alpha have no `users.json` yet — on the first visit `/login` renders a setup form (username + password + password-confirm) that creates the admin record on submit. Fresh installs skip this: the webinstaller writes `users.json` during the chroot post-install step using the step-1 password, so the first browser visit after boot goes straight to the login form. - **Caddy proxy routes `/login` and `/logout`.** `assets/Caddyfile` gets two new `handle` blocks in the shared `(furtka_routes)` snippet so both the `:80` block and the `hostname.local, hostname` HTTPS block forward the auth endpoints to the stdlib server on `127.0.0.1:7000`. Without this Caddy would serve a 404 from the static file server. ### Fixed - `tests/test_installer.py` ruff-format nit — the 26.10-alpha release commit had a misformatted list literal that failed `ruff format --check`. Caught when the Release page on Forgejo showed a red CI badge for the tag. - `pyproject.toml` version string bumped from the stale 26.8-alpha to 26.11-alpha. Release pipeline uses `GITHUB_REF_NAME` as source of truth for the artefact name, but having the two agree matters for local dev runs that read `pyproject.toml`. ## [26.10-alpha] - 2026-04-21 ### Added - **Remove-USB-stick hint on the installer's post-install screen.** `webinstaller/templates/install/rebooting.html` now shows a bold "Remove the USB stick now" line before the reboot, plus a muted fallback explaining the BIOS boot-menu keys (F11/F12/Esc) if the machine boots back into the installer anyway. Caught on the first bare-metal test (Medion i5-4gen, 2026-04-21) where the box didn't boot the installed system without manual BIOS-order changes. - **New `path` setting type for app manifests.** Apps can now declare a setting with `"type": "path"` whose value is an absolute filesystem path on the host; docker-compose bind-mounts it via the usual `.env` substitution (`${MEDIA_PATH}:/media`). Unlocks media/data-heavy apps (Jellyfin, later Paperless/Nextcloud/Immich) where the user points at an existing folder instead of copying everything into a Docker volume. The install form renders path settings as a plain text input with a `/mnt/…` placeholder hint. - **Server-side path validation.** Both `install_from()` and `update_env()` refuse values that aren't absolute, don't exist, aren't directories, or resolve (after `Path.resolve()`) into a system-path deny-list (`/`, `/etc`, `/root`, `/boot`, `/proc`, `/sys`, `/dev`, `/bin`, `/sbin`, `/usr/bin`, `/usr/sbin`, `/var/lib/furtka`). Catches `/mnt/../etc`-style traversal too. Error messages surface in the existing install/edit modal error line. ## [26.9-alpha] - 2026-04-21 ### Fixed - Landing-page app tiles with an `open_url` now open in a new tab (`target="_blank" rel="noopener"`), matching the Open button behaviour on `/apps`. Without this, clicking "Uptime Kuma" on the home screen replaced Furtka itself with the Kuma admin page. Internal links (the `Manage →` fallback for apps without an `open_url`) still open in the same tab. - `scripts/publish-release.sh` no longer fails the whole release when the ISO upload hits a Forgejo proxy 504. The core tarball + sha256 + release.json (which running boxes need for self-update) are uploaded first and the ISO is attempted last as a best-effort; a 504 now logs a warning and exits 0 so the release page still publishes. Surfaced by the 26.8-alpha cut: the tarball landed but the ~1 GB ISO upload timed out at the Forgejo reverse proxy. ### Changed - `furtka app list --json` now mirrors `/api/apps` field-for-field — previously the CLI emitted a slim projection missing `description_long`, `open_url`, and `settings`. Anyone piping the CLI output into jq for automation was seeing an incomplete view. ## [26.8-alpha] - 2026-04-20 ### Added - **Live-installer ISO attached to the Forgejo release page.** `.forgejo/workflows/release.yml` moves to the self-hosted runner, builds both the self-update tarball and the ISO, and `scripts/publish-release.sh` uploads the ISO as a fourth release asset (`furtka-.iso`) alongside the existing tarball + sha256 + release.json. Fresh-install users can now grab the ISO from the release page instead of hunting through `build-iso.yml` artifact retention windows. ISO build step is `continue-on-error` so an ISO flake doesn't hold back the core tarball that running boxes need for self-update. - **Reboot + Shut down buttons on `/settings`.** Replaces the two "Coming next" placeholders with real actions backed by `POST /api/furtka/power` (`{"action": "reboot" | "poweroff"}`). Handler kicks a delayed `systemd-run --on-active=3s systemctl {reboot|poweroff}` so the HTTP response reaches the browser before the kernel loses network. Each button opens a native confirm dialog first (reboot: "back in ~30 s", shut down: "need to press the physical power button"), then the UI swaps to a status line and — after a reboot — polls `/furtka.json` until the box is back, reloading the page automatically. No auth (same posture as install/remove). - **Manifest `open_url` field + Open button in `/apps` and on the landing page.** Apps declare a URL template (e.g. `smb://{host}/files` for fileshare, `http://{host}:3001/` for Uptime Kuma); the UI substitutes `{host}` with the current browser's hostname at render time so the link follows however the user reached Furtka (furtka.local, raw IP, a future reverse-proxy hostname). The landing page's hardcoded `if app.name === 'fileshare'` special-case is gone — any app with an `open_url` in its manifest now gets a proper "Open" link. The core seed `apps/fileshare/manifest.json` bumps to v0.1.2 to carry it. ### Changed - `.btn` CSS class introduced so an `` rendered-as-button lines up with its `