Commit graph

120 commits

Author SHA1 Message Date
470823b347 feat(auth): login-guard the Furtka UI with a cookie session
All checks were successful
Build ISO / build-iso (push) Successful in 17m30s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 43s
CI / validate-json (push) Successful in 31s
CI / markdown-links (push) Successful in 15s
Release / release (push) Successful in 11m38s
One-admin, one-password model — all of /apps, /api/*, /, and
/settings/ now require a signed-in session. Passwords are werkzeug
PBKDF2-hashed in /var/lib/furtka/users.json (mode 0600, atomic write
via the same .tmp+chmod+rename dance installer.write_env uses).
Sessions are secrets.token_urlsafe(32) tokens held in a module-level
SessionStore dict (thread-safe lock included for when we swap to
ThreadingHTTPServer). Cookies are HttpOnly, SameSite=Strict, and
Path=/, with Secure set when X-Forwarded-Proto from Caddy says HTTPS.

Two bootstrap paths:
  * Fresh install — webinstaller step-1 collects Linux user + password,
    the chroot post-install step hashes the password and writes
    users.json on the target partition. First browser visit lands on
    /login with the account already present.
  * Upgrade from 26.10-alpha — no users.json yet, so /login detects
    setup_needed() and renders a first-run setup form. POST creates
    the admin and immediately logs in.

POST /logout revokes the server session and clears the cookie.
Unauthenticated HTML requests 302 to /login; unauthenticated API
requests 401 JSON so fetch() callers see a clean error. A sleep(0.5)
on failed logins is the brute-force speed bump on top of werkzeug's
~600k-iter PBKDF2.

Caddyfile gains /login* and /logout* handle blocks in the shared
furtka_routes snippet so both :80 and the HTTPS hostname block
forward the auth endpoints to localhost:7000. Without this Caddy
would 404 from the static file server.

Test surface:
  * tests/test_auth.py (new, 19 cases): hash roundtrip, users.json
    I/O, session create/lookup/expire/revoke.
  * tests/test_api.py: new admin_session fixture; existing HTTP
    tests updated to send the cookie; new tests cover login setup,
    login success, wrong-password 401, logout revocation, and the
    guard's 302/401 split.
  * tests/test_webinstaller_assets.py: new case that unpacks the
    users.json _write_file_cmd body and verifies the werkzeug hash
    round-trips against the step-1 password.

Bumped version to 26.11-alpha and rolled CHANGELOG. Also folded in
the ruff-format fix that was pending from 26.10-alpha's lint red.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:01:17 +02:00
577c2469f7 style(tests): reflow OPTIONAL_PATH_MANIFEST to match ruff format
All checks were successful
Build ISO / build-iso (push) Successful in 20m27s
CI / lint (push) Successful in 29s
CI / test (push) Successful in 1m3s
CI / validate-json (push) Successful in 46s
CI / markdown-links (push) Successful in 23s
Fixes the lint failure on the 26.10-alpha commit — ruff format wanted
the single-item settings list on one line rather than spread over
three. Pure formatting, no behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:56:52 +02:00
e8c5317660 chore: release 26.10-alpha
Some checks failed
CI / lint (push) Failing after 50s
CI / test (push) Successful in 1m6s
CI / validate-json (push) Successful in 42s
CI / markdown-links (push) Successful in 22s
Release / release (push) Successful in 13m27s
Ships the new path-type setting (the schema extension that unlocks
host bind mounts for Jellyfin / Paperless / Nextcloud / Immich-class
apps), server-side path validation, app-author docs for the new type,
and the remove-USB-stick hint on the installer's reboot screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:48:07 +02:00
474af8fb2d feat(installer): remove-USB-stick hint on the reboot screen
Some checks failed
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Failing after 4m15s
Adds a bold "Remove the USB stick now" line before the reboot, plus a
muted fallback paragraph pointing at the BIOS one-time boot menu keys
(F11/F12/Esc) for when removal isn't enough. Caught on the 2026-04-21
Medion bare-metal test: the box didn't boot the installed system on
first reboot and required manual BIOS boot-order changes, which
non-technical users won't know how to do.

Template-only change. No new CSS, no new code paths — <kbd> uses
browser defaults, <strong> keeps the hierarchy readable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:46:38 +02:00
7c6da3d051 docs(apps): document the new path setting type
Some checks failed
CI / lint (push) Failing after 38s
CI / test (push) Successful in 54s
CI / validate-json (push) Successful in 34s
CI / markdown-links (push) Successful in 19s
Covers the path-type declaration in manifest.json, the companion
compose bind-mount pattern (${MEDIA_PATH}:/media:ro), and the full
server-side validation rules the installer applies (absolute, exists,
is-directory, resolve-then-deny-list, traversal caught).

Clarifies the mental split between manifest.volumes (internal state
the app owns) and path settings (user data the container mounts and
usually reads without owning), and recommends :ro as the default for
consumer-only mounts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:43:09 +02:00
04762f5dd1 feat(manifest): add 'path' setting type with server-side validation
Some checks failed
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Failing after 4m34s
Apps can now declare a setting with "type": "path" whose value is an
absolute host filesystem path. Compose bind-mounts it via standard .env
substitution (${MEDIA_PATH}:/media) — no reconciler changes needed.
Unlocks media/data-heavy apps (Jellyfin, later Paperless, Nextcloud,
Immich) that point at existing user data instead of copying it into a
Docker volume.

Install/update refuses values that aren't absolute, don't exist, aren't
directories, or resolve into a system-path deny-list (/, /etc, /root,
/boot, /proc, /sys, /dev, /bin, /sbin, /usr/bin, /usr/sbin,
/var/lib/furtka). Path.resolve() is applied before the deny-list check
so /mnt/../etc traversal is caught too. Error messages surface in the
existing install/edit modal.

UI: path settings render as a text input with a /mnt/… placeholder.
The manifest's `description` field carries the actual hint ("Absoluter
Pfad zu deinem Filme-Ordner, z.B. /mnt/media"). No new form
components, no new API routes.

Tests: 9 new cases for install + update path validation; 1 new case
for manifest schema accepting the path type. 211 total passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:39:15 +02:00
c7e7c8b1e5 chore: release 26.9-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 20m49s
CI / lint (push) Successful in 1m13s
CI / test (push) Successful in 48s
CI / validate-json (push) Successful in 44s
CI / markdown-links (push) Successful in 16s
Release / release (push) Successful in 13m31s
Three small fixes surfaced by the 26.8 QA pass on fresh VM .161:

- Landing-page app tiles now open external `open_url` links in a new
  tab, matching /apps Open-button behaviour. Without this a Kuma click
  on the home screen replaced Furtka itself.
- `scripts/publish-release.sh` treats the ISO upload as best-effort;
  a Forgejo-proxy 504 no longer kills the whole release after tarball
  + sha + release.json are already uploaded.
- `furtka app list --json` now mirrors /api/apps — includes
  `description_long`, `open_url`, and `settings` that the previous
  slim projection dropped.
2026-04-20 18:51:30 +02:00
cf93ef44cb chore: release 26.8-alpha (power actions, supersedes orphan 26.7 tag)
Some checks failed
Build ISO / build-iso (push) Successful in 26m56s
Deploy site / deploy (push) Successful in 23s
CI / lint (push) Successful in 34s
CI / test (push) Successful in 1m4s
CI / validate-json (push) Successful in 51s
CI / markdown-links (push) Successful in 28s
Release / release (push) Failing after 7m38s
Adds Reboot + Shut down buttons on /settings, backed by a new
POST /api/furtka/power endpoint that kicks a delayed `systemd-run
--on-active=3s systemctl {reboot|poweroff}` so the HTTP response
flushes before the kernel loses network. Both buttons open a native
confirm dialog; after reboot, the page polls /furtka.json until the
box is back and reloads itself.

26.7-alpha was tagged on 5d8ac63 but release.yml never fired for that
tag (Forgejo race with the concurrent main push; re-push of the deleted
tag didn't wake the workflow either). 26.8 supersedes it and carries
the same open_url + Open-button content plus the power actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:00:19 +02:00
5d8ac63d9f chore: release 26.7-alpha
Some checks failed
Deploy site / deploy (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
CI / lint (push) Successful in 1m26s
CI / test (push) Successful in 1m18s
CI / validate-json (push) Successful in 52s
CI / markdown-links (push) Successful in 27s
Release / release (push) Has been cancelled
Ships the open_url manifest field + the Open button in /apps and on
the landing page, replacing the fileshare-only hardcoded deep-link
with a generalised {host}-templated URL. Fileshare seed manifest
bumps to 0.1.2; the furtka-apps catalog release that goes with this
adds matching open_url values for fileshare + uptime-kuma.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:44:01 +02:00
018f2e20b0 chore: release 26.6-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 21m23s
CI / lint (push) Successful in 1m31s
CI / test (push) Successful in 1m20s
CI / validate-json (push) Successful in 48s
CI / markdown-links (push) Successful in 27s
Deploy site / deploy (push) Successful in 8s
Release / release (push) Successful in 24s
Rolls the apps-catalog split, the /settings CSS wrap fix, and the version
bump to 26.6-alpha across pyproject + website copy. Core release tarball
still carries apps/fileshare as the offline first-boot seed; the new
daniel/furtka-apps catalog (tagged 26.6-alpha today) is the authoritative
source on boxes that have synced at least once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:49:31 +02:00
3a8fad5185 feat(catalog): on-box apps catalog synced independently of core version
New `furtka catalog sync` pulls the latest daniel/furtka-apps release,
verifies its sha256, extracts under /var/lib/furtka/catalog/, and
atomically swaps into place — so apps can ship without cutting a new
Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min
post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a
manual "Sync apps catalog" button that kicks the same code path via a
detached systemd-run unit.

Layout of the new on-box tree:

  /var/lib/furtka/catalog/            synced catalog (survives self-updates)
    ├── VERSION
    └── apps/<name>/ ...
  /var/lib/furtka/catalog-state.json  sync stage + last version, UI-polled
  /run/furtka/catalog.lock            flock so timer + manual click can't race

Resolver precedence (furtka/sources.py): catalog wins over the bundled
seed (/opt/furtka/current/apps/, carried by the core release for offline
first-boot). Installed apps under /var/lib/furtka/apps/ are never auto-
swapped — user clicks Reinstall to move an existing install onto a
newer catalog version; settings merge-preserved via the existing
installer.install_from path.

New files:
- furtka/_release_common.py — shared Forgejo/tarball primitives lifted
  from furtka/updater.py. Both modules now import from here; updater's
  behaviour and public API unchanged.
- furtka/catalog.py — check_catalog(), sync_catalog() with staging +
  manifest validation + atomic rename. Refuses bad sha256 / broken
  manifests and leaves the live catalog intact on any failure path.
- furtka/sources.py — resolve_app_name() / list_available() abstraction
  used by installer.resolve_source and api._list_available.
- assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service
  + daily timer. Timer auto-enables on self-update via a one-line
  addition to _link_new_units (fresh installs get enabled via the
  webinstaller's _FURTKA_UNITS list).

API + UI:
- /api/bundled renamed internally to _list_available; endpoint stays as
  a backcompat alias; /api/apps/available is the new canonical name.
  Each list entry carries a `source` field ("catalog" | "bundled").
- POST /api/catalog/sync/check + /apply + GET /api/catalog/status.
- /apps page grows a catalog-status row + Sync button; poll loop
  mirrors the Furtka self-update flow.

CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both
support --json). Old `furtka app install` / `reconcile` / `update` /
`rollback` surfaces are unchanged.

Test gate: 194/170 baseline + 24 new tests covering catalog sync
(happy path, sha256 mismatch, invalid manifest, lock contention,
preserves-on-failure) + resolver precedence + api renames. ruff
check + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
e7ee1698bd fix(ui): stop SHA-256 fingerprint overflowing the Local HTTPS card
The /settings "CA fingerprint (SHA-256)" value is a 95-char colon-
separated hex string with no whitespace, so CSS had no valid break
points and the value pushed past the card's right edge — visible on
the 192.168.178.23 fresh-install test.

.kv is a two-column grid (max-content 1fr); grid items default to
min-width: auto (= content width), which overrides the 1fr track's
width constraint. min-width: 0 lets the track shrink, and
overflow-wrap: anywhere gives the fingerprint valid break points at
any character. The styling stays scoped to .kv dd so card prose isn't
affected.

Verified live on .23 via hot-patch into /opt/furtka/current/assets/
www/style.css + caddy reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:41:33 +02:00
54357aa2a3 style: ruff format — collapse two-line hostname file path + version loop
All checks were successful
Build ISO / build-iso (push) Successful in 21m29s
CI / lint (push) Successful in 37s
CI / test (push) Successful in 58s
CI / validate-json (push) Successful in 42s
CI / markdown-links (push) Successful in 23s
Format-only diff from `ruff format`. The 26.5-alpha push's CI run failed
on `ruff format --check`; these three files had two-line constructs that
fit on one line at ruff's default line length. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:41:58 +02:00
fec962e3d2 chore: release 26.5-alpha
Some checks failed
Build ISO / build-iso (push) Successful in 20m10s
Deploy site / deploy (push) Successful in 13s
CI / lint (push) Failing after 26s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 14s
Release / release (push) Successful in 6s
Rolls the HTTPS handshake fix (#10) and the README realignment into a
tagged release. Also closes the 26.4 follow-up that the wizard footer
version was hand-pinned: webinstaller/app.py now resolves the version
via a Flask context processor (reads /opt/furtka/VERSION on the live
ISO, written by iso/build.sh from pyproject.toml at build time; falls
back to pyproject.toml in dev runs, then to "dev"). pyproject.toml and
the website version strings bumped in the same commit so every surface
reports 26.5-alpha consistently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:52:36 +02:00
8fbe67ffb9 fix(https): restore TLS handshake — name hostname + correct PKI path
Some checks failed
Build ISO / build-iso (push) Waiting to run
CI / lint (push) Failing after 2m11s
CI / test (push) Successful in 2m8s
CI / validate-json (push) Successful in 55s
CI / markdown-links (push) Successful in 25s
Deploy site / deploy (push) Successful in 8s
Closes #10. Two linked bugs in 26.4-alpha's Phase 1 HTTPS made the
force-HTTPS toggle fatal: every SNI handshake on :443 died with
SSL_ERROR_INTERNAL_ERROR_ALERT, so the toggle redirected users from
working HTTP to broken HTTPS.

Root cause 1: bare `:443 { tls internal }` gives Caddy no hostname to
issue a leaf cert for, so /var/lib/caddy/certificates/ stayed empty and
Caddy sent TLS `internal_error` on every handshake. Fix: the :443 block
is now `__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ { tls internal }`,
with the marker substituted by webinstaller/app.py at install time and
by furtka.updater._refresh_caddyfile on self-update (reads /etc/hostname,
falls back to "furtka"). `auto_https disable_redirects` keeps Caddy's
built-in redirect out of the way of the /settings toggle.

Root cause 2: furtka/https.py and the /rootCA.crt handler both referenced
/var/lib/caddy/.local/share/caddy/pki/authorities/local/ — a path that
doesn't exist. caddy.service sets XDG_DATA_HOME=/var/lib, so Caddy's
storage is /var/lib/caddy/ directly. Fix: both paths corrected.

Verified on the 192.168.178.110 smoke VM by swapping the Caddyfile in,
reloading, handshaking, restoring: TLS 1.3 handshake succeeds, leaf cert
issued under /var/lib/caddy/certificates/local/, /rootCA.crt returns 200.

Tests: new cases assert the Caddyfile ships the hostname placeholder,
the webinstaller substitutes it, _refresh_caddyfile re-substitutes from
/etc/hostname on update, and the asset sets auto_https disable_redirects.
Unit tests still stub the Caddy reload — the real handshake regression
needs a smoke-VM integration test (follow-up, separate from this fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:39:48 +02:00
9ae14f4108 docs: add apps/ authoring guide + realign READMEs with 26.4-alpha
Closes #9. New apps/README.md walks through the four-file contract
(manifest.json, docker-compose.yaml, .env.example, icon.svg) with
the rules enforced by furtka/manifest.py and the SVG sanitiser, using
apps/fileshare as the reference.

Root README: release list now covers 26.1/26.3/26.4 (26.2 stalled on
the jq apt hang). Local HTTPS Phase 1 and the post-build smoke VM on
pollux both flip to [x]; the old proksi.local HTTPS TODO becomes a
Phase 2 entry (dedicated local CA + HTTPS on the live-installer wizard).

iso/README: mDNS is wired — live ISO advertises proksi.local, installed
box defaults to furtka.local (the form's default hostname, not proksi).
HTTPS section notes Caddy tls internal on :443 shipped in 26.4 while
the wizard itself is still HTTP. Overlay table picks up etc/hostname,
etc/issue, furtka-update-issue, and furtka-issue.service.

website/README: auto-deploy via .forgejo/workflows/deploy-site.yml is
the default path now; website/deploy.sh stays as the SSH-hop fallback
for off-CI pushes, and deploy-ci.sh is called out in the structure map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:39:48 +02:00
850d656169 Merge pull request 'fix(smoke): capture arp-scan output instead of piping into awk' (#8) from fix-smoke-pipefail into main
All checks were successful
Build ISO / build-iso (push) Successful in 17m6s
CI / lint (push) Successful in 26s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 28s
CI / markdown-links (push) Successful in 13s
Reviewed-on: #8
2026-04-18 15:43:50 +02:00
93c6b838a7 fix(smoke): capture arp-scan output instead of piping into awk
All checks were successful
CI / lint (pull_request) Successful in 26s
CI / test (pull_request) Successful in 34s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 14s
When host-networking finally gave arp-scan a real LAN to scan, the
first MAC-match emitted a line, awk hit its `exit` clause, closed the
pipe, and arp-scan died from SIGPIPE (exit 141). With `set -o pipefail`
active, that killed the whole smoke-vm.sh run immediately after
"==> starting VM" — no IP discovery, no curl, no prune.

Fix: capture arp-scan's output into a variable first, then let awk
parse a here-string. Same treatment for the `ip neigh show` fallback.
No pipe, no pipefail cascade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:26:10 +02:00
caa8609908 Merge pull request 'release-26.4-alpha' (#7) from release-26.4-alpha into main
Some checks failed
Build ISO / build-iso (push) Successful in 26m22s
Deploy site / deploy (push) Successful in 3s
CI / lint (push) Successful in 26s
CI / test (push) Successful in 1m37s
CI / markdown-links (push) Successful in 33s
Release / release (push) Successful in 6s
CI / validate-json (push) Failing after 14m0s
Reviewed-on: #7
2026-04-18 14:29:19 +02:00
522ea06cd0 fix(smoke): bump smoke-VM RAM to 8 GiB + make cores/memory configurable
All checks were successful
CI / lint (pull_request) Successful in 1m10s
CI / test (pull_request) Successful in 2m17s
CI / validate-json (pull_request) Successful in 1m5s
CI / markdown-links (pull_request) Successful in 41s
pollux (192.168.178.165) wedged at the network level during an
end-to-end install test today — mkinitcpio on a 4 GiB smoke VM +
the cached 1.5 GB ISO + a busy runner container pushed the host into
OOM, taking pveproxy and the SSH path down with it. Recovered by
physical reset.

Smoke VM now defaults to 8192 MiB / 2 vCPU, configurable via
PVE_TEST_VM_MEMORY / PVE_TEST_VM_CORES. Host has 64 GiB, so one
smoke VM at 8 GiB is well within headroom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:28:29 +02:00
d567317538 chore: release 26.4-alpha
Bumps version everywhere user-facing that had drifted from the tag:

- pyproject.toml 26.0 → 26.4
- website/hugo.toml 26.0 → 26.4 (driving furtka.org landing + footer)
- website/content/_index{.md,.de.md} status string
- webinstaller/templates/base.html footer (was hardcoded — noted as
  follow-up to read dynamically from pyproject.toml)

Promotes the Unreleased section to 26.4-alpha and folds in today's
additions:

- Local HTTPS via Caddy tls internal + opt-in redirect toggle
- Two self-update UX fixes (Installed-field refresh + 45s reload
  fallback)
- Impressum + Datenschutzerklärung on furtka.org
- deploy-site.yml auto-deploy of the Hugo site on push-to-main
- Smoke VM pipeline on .165 Proxmox (build-iso inline smoke step +
  workflow_dispatch Smoke latest ISO for cheap re-tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:21:43 +02:00
931d62149f Merge pull request 'chore(smoke): surface PVE response body on API failure' (#6) from debug-smoke-errors into main
Some checks failed
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Reviewed-on: #6
2026-04-18 14:06:47 +02:00
f4f7d853ba chore(smoke): surface PVE response body on API failure
Some checks failed
CI / lint (pull_request) Successful in 1m3s
CI / test (pull_request) Successful in 1m23s
CI / markdown-links (pull_request) Has been cancelled
CI / validate-json (pull_request) Has been cancelled
api() was swallowing Proxmox's error body because callers pipe its
output to /dev/null. With a bare "curl: (22) 403" in the log we can't
tell which permission is missing. Now we capture the response body,
print it to stderr on failure, and only emit it to stdout on success.

No behaviour change on the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:06:09 +02:00
cb6e92aa92 Merge pull request 'fix(smoke): reuse existing PVE-side ISO instead of delete+re-upload' (#5) from fix-smoke-reuse-iso into main
Some checks failed
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Reviewed-on: #5
2026-04-18 14:00:40 +02:00
afbb8d59f9 fix(smoke): reuse existing PVE-side ISO instead of delete+re-upload
Some checks failed
CI / markdown-links (pull_request) Waiting to run
CI / lint (pull_request) Successful in 1m5s
CI / validate-json (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
The delete branch required Datastore.Allocate (or was hitting a
privilege-separated token ACL edge case) and produced 403s on re-runs
against the same commit SHA. Since the ISO bytes are reproducible for
a given SHA — furtka-<sha>.iso is content-addressed — we can just
reuse whatever is already in PVE storage instead of cycling it.

Fixes the "runs-on-same-sha" re-dispatch case without needing any extra
PVE permission, and shaves ~2 min off repeated smoke runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:59:42 +02:00
2cfe54e03a Merge pull request 'fix(ci): apk-install smoke prerequisites before running smoke-vm.sh' (#4) from fix-smoke-deps into main
All checks were successful
Build ISO / build-iso (push) Successful in 22m40s
CI / lint (push) Successful in 1m4s
CI / test (push) Successful in 1m24s
CI / validate-json (push) Successful in 55s
CI / markdown-links (push) Successful in 26s
Reviewed-on: #4
2026-04-18 13:20:52 +02:00
1d75a165c4 fix(ci): apk-install smoke prerequisites before running smoke-vm.sh
All checks were successful
CI / lint (pull_request) Successful in 2m2s
CI / test (pull_request) Successful in 1m23s
CI / validate-json (pull_request) Successful in 58s
CI / markdown-links (pull_request) Successful in 26s
The Forgejo runner container is Alpine with a near-empty base — no
curl, python3, arp-scan, or sudo out of the box. scripts/smoke-vm.sh
needs all four:
  - curl: every PVE API call
  - python3: JSON parsing of PVE responses
  - arp-scan: MAC→IP discovery on the LAN (live ISO has no guest agent)
  - sudo: so the same script also works from a dev laptop as non-root

Without this step the smoke job fails immediately on "curl: not found",
regardless of whether the PVE secrets are correctly set.

Added to both build-iso.yml (inline smoke after ISO build) and
smoke-latest.yml (workflow_dispatch retest path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:17:51 +02:00
2cc3fab027 Merge pull request 'feat(ci): workflow_dispatch smoke-latest + cache ISO for fast retests' (#3) from feat-smoke-latest into main
Some checks failed
Build ISO / build-iso (push) Has been cancelled
CI / test (push) Has been cancelled
CI / validate-json (push) Has been cancelled
CI / markdown-links (push) Has been cancelled
CI / lint (push) Has been cancelled
Reviewed-on: #3
2026-04-18 13:11:41 +02:00
41d0e7a398 feat(ci): workflow_dispatch smoke-latest + cache ISO for fast retests
Some checks failed
CI / lint (pull_request) Successful in 2m6s
CI / test (pull_request) Successful in 3m23s
CI / validate-json (pull_request) Has been cancelled
CI / markdown-links (pull_request) Has been cancelled
When smoke-vm.sh / PVE setup / secrets change, we want to verify the
fix without waiting for a full 25-min build-iso rebuild (most of which
is the upload-artifact step for a 1.5 GB file).

Adds two things:

1. build-iso.yml grows a "Cache ISO for smoke-latest" step that copies
   the freshly built ISO to /data/smoke-cache/latest.iso. /data is
   already bind-mounted into the runner container at a matching host
   path, so no compose.yml change or runner restart needed.

2. smoke-latest.yml is a workflow_dispatch-only workflow that reads
   /data/smoke-cache/latest.iso and runs scripts/smoke-vm.sh against
   it. ~2 min end-to-end. Errors cleanly if the cache is empty (build-
   iso.yml hasn't populated it yet).

First build-iso run after this merges will populate the cache; from
then on smoke-latest is available for on-demand re-tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:04:22 +02:00
a511f5418d Merge pull request 'feat(website): legal pages (Impressum/Datenschutz) + auto-deploy on push-to-main' (#1) from website-legal into main
Some checks are pending
CI / test (push) Waiting to run
Build ISO / build-iso (push) Successful in 24m57s
CI / lint (push) Successful in 2m17s
CI / validate-json (push) Successful in 2m0s
CI / markdown-links (push) Successful in 1m52s
Deploy site / deploy (push) Successful in 15s
Reviewed-on: #1
2026-04-18 12:32:15 +02:00
cf85217c0d Merge pull request 'fix(ci): inline smoke-vm as a step instead of a downstream job' (#2) from fix-smoke-inline into main
Some checks are pending
Build ISO / build-iso (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Reviewed-on: #2
2026-04-18 12:31:50 +02:00
7b894f096f fix(ci): inline smoke-vm as a step instead of a downstream job
All checks were successful
CI / lint (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 1m23s
CI / validate-json (pull_request) Successful in 56s
CI / markdown-links (pull_request) Successful in 24s
The separate smoke-vm job with `needs: build-iso` required round-tripping
the 1.5 GB ISO through actions/upload-artifact + download-artifact. v3
on Forgejo has a known issue where large artifacts stall at 0.0% in the
download step — the smoke run hung today with endless "Total file count:
1 ---- Processed file #0 (0.0%)" output.

Since both jobs run on the same self-hosted runner (host mode, same
workspace available), there was never a real need for the artifact
indirection. Inlining as a step after the artifact upload reuses the
ISO already in iso/out/ and skips the download entirely.

step-level continue-on-error preserves the original guarantee that a
VM-side flake doesn't mark the ISO build red.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:20:58 +02:00
b77ef80b56 feat(website): legal pages (Impressum/Datenschutz) + auto-deploy on push-to-main
All checks were successful
CI / lint (pull_request) Successful in 1m2s
CI / test (pull_request) Successful in 1m19s
CI / validate-json (pull_request) Successful in 55s
CI / markdown-links (pull_request) Successful in 27s
Two coupled changes that make sense to land together:

1. Legal pages required under German law
   - /imprint/ + /de/impressum/ — §5 DDG disclosure (contact is email
     plus Forgejo-Issues as the second quick-contact channel, per ECJ
     C-298/07 no phone number required)
   - /privacy/ + /de/datenschutz/ — Art. 13 GDPR minimum: server-log
     processing (IP, UA, URL, retention ≤30 days), no cookies, no
     tracking, no third-party embeds. RLP Landesbeauftragter as the
     competent supervisory authority.
   - Footer partial linked from every page, localized per language.
   - DE versions are legally binding; EN versions are courtesy
     translations noting that.

2. Auto-deploy wired up
   - New workflow .forgejo/workflows/deploy-site.yml fires on
     push-to-main with paths under website/**. Runs on the self-hosted
     runner, which *is* forge-runner-01 — so "deploy" is just a local
     rsync into /srv/furtka-site and a hugo build into
     /var/www/furtka.org. No SSH, no secrets.
   - website/deploy-ci.sh is the SSH-free counterpart of deploy.sh,
     invoked by the workflow.
   - compose.yml bind-mounts /srv/furtka-site and /var/www/furtka.org
     into the runner container at matching paths so the workflow can
     reach them. Requires a one-time `docker compose up -d` on the
     runner host to pick the mounts up.
   - deploy.sh is kept for out-of-band manual deploys (testing from a
     local branch, CI outage) but gets a header comment pointing at
     the CI path as the normal flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:10:06 +02:00
d499907613 feat(ci): auto-boot every main-ISO in smoke VM on .165 Proxmox
Some checks failed
Build ISO / smoke-vm (push) Blocked by required conditions
Build ISO / build-iso (push) Successful in 24m28s
CI / test (push) Successful in 3m1s
CI / validate-json (push) Successful in 55s
CI / markdown-links (push) Successful in 37s
CI / lint (push) Failing after 13m19s
After build-iso, a new smoke-vm job uploads the freshly built ISO to
the test Proxmox at 192.168.178.165 via PVE API token, boots it in a
fresh VM (VMID range 9000-9099, MAC derived from commit SHA so the
runner can find the DHCP IP by scanning the LAN), and curls :5000 to
confirm the webinstaller answers HTTP 200. Last 5 smoke VMs + their
ISOs are kept for post-mortem; older ones are purged. continue-on-error
on the smoke job so a VM-side flake doesn't mark the ISO build red.

Shortens the feedback loop on ISO regressions from "next manual VM
test session" (days) to "next push" (minutes) — the 2026-04-15/16 VM
sessions each found real boot-time bugs that unit tests missed.

Docs at docs/smoke-vm.md. Requires Forgejo secrets PVE_TEST_HOST and
PVE_TEST_TOKEN (dedicated smoke@pve!ci PVE token, privilege-separated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:41:44 +02:00
3f7b97c8c7 style: ruff format two files the pre-commit hook caught
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:41:28 +02:00
663bd74572 feat(https): local HTTPS via Caddy tls internal + opt-in redirect toggle
Some checks failed
Build ISO / build-iso (push) Successful in 20m57s
CI / lint (push) Failing after 31s
CI / test (push) Successful in 36s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 14s
Caddy now serves both :80 (plain HTTP, unchanged default) and :443 with
tls internal — it generates its own per-box root CA on first start,
stored under /var/lib/caddy/.local/share/caddy/pki/authorities/local/.
Users can download rootCA.crt at /rootCA.crt (served on both listeners)
and install it per-OS via the new /https-install/ guide.

Settings page grows a Local HTTPS card with CA fingerprint, download
button, reachability probe, and an opt-in "force HTTPS" toggle. The
toggle only unhides itself once the current browser already trusts the
cert, so enabling it can't lock the user out of the settings page.

Backend: GET /api/furtka/https/status and POST /api/furtka/https/force
in furtka.https. The force toggle drops a Caddy import snippet into
/etc/caddy/furtka.d/redirect.caddyfile and reloads Caddy; reload
failure rolls the snippet state back so a bad config can't wedge the
next service start.

updater._refresh_caddyfile() ensures /etc/caddy/furtka.d exists before
every reload so 26.3-alpha → 26.4-alpha self-updates don't trip on the
new glob import directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:19:06 +02:00
a5de3d7622 fix(settings): close the two self-update UX gaps from 2026-04-16 VM test
Drive upd-current from the /api/furtka/update/check response so a
post-update Check reflects the new installed version without Ctrl+F5,
and arm a 45s fallback location.reload on apply-click so the page still
comes up on the new version when the mid-apply API restart drops the
/update-state.json poll before stage=done is observed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:22:34 +02:00
bf86ffaf4c docs(website): ship the two update bullets — validated on VM today
All checks were successful
CI / lint (push) Successful in 26s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 12s
End-to-end validation of the per-app container update and the Furtka
self-update ran green on VM 192.168.178.128 this afternoon (26.0-alpha
→ 26.3-alpha → rollback → reboot). Both flows are real — promote the
drafted HTML-comment bullets from _index.md and _index.de.md into the
visible "What works today" list.

The "plain-English Wi-Fi story" was the only one the copy was missing
a truthful on-box outcome for, and it still is (for a moment here
a-few-days-ago, but the update story moved past that).

Matches the commitment in feedback_no_invented_content.md — we only
publish after confirmation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:44:51 +02:00
8de8f3fd87 docs(readme): roadmap through 2026-04-16 — resource mgr, UI, self-update
All checks were successful
CI / lint (push) Successful in 36s
CI / test (push) Successful in 34s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 13s
Roadmap section drifted far enough that "re-tag 26.0-alpha" was still
listed as open while 26.1-alpha and 26.3-alpha are live releases.

Updated:
- Replaced the stale "re-tag 26.0-alpha" line with the actual state:
  tag-driven release pipeline is wired, two pre-releases published,
  all assets downloadable anonymously.
- Added five new checked items for the work that landed this month:
  resource manager + fileshare (validated), on-box UI uplevel (shared
  CSS / settings page / icons), versioned layout + per-app container
  updates, Phase 2 Furtka self-update (tag → release.yml → /settings
  Update now → atomic swap + auto-rollback), plus the broader Forgejo
  release pipeline that underpins the update story.
- Kept open items (wizard S3-S7, managed gateway, Authentik, local CA,
  Nextcloud first service, UI mockups) as the remaining TODO surface.

No code or test changes; pytest + ruff still green from the last push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:40:25 +02:00
25bef628c2 docs(changelog): note two /settings update-flow UX gaps for next release
All checks were successful
CI / lint (push) Successful in 26s
CI / test (push) Successful in 34s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 12s
End-to-end validated the Phase-2 self-update today on a fresh install
(192.168.178.128 → 26.0-alpha → 26.3-alpha): the symlink flip, the
tarball verify, the stage-by-stage progress, and the rollback slots
all work. But two browser-side UX bits are rough:

1. The "Installed" version displayed on /settings doesn't refresh
   right after the update; a hard reload shows the new value.
2. The auto-reload that should fire 5s after stage=done missed on
   the test — the polling connection likely dropped during the
   mid-update API restart.

Neither affects the integrity of the update itself. Landed the notes
in [Unreleased] so the next release cycle picks them up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:31:41 +02:00
b4c65f46bf fix(release): drop jq dependency, use python3 for JSON assembly
All checks were successful
Build ISO / build-iso (push) Successful in 17m30s
CI / lint (push) Successful in 25s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 12s
Release / release (push) Successful in 6s
The 26.2-alpha release workflow hung for 15+ minutes on
"apt-get install -y jq" — the runner's apt mirror was unreachable
(or very slow), and the whole publish stalled.

jq was only used for two tiny things: building the release-create
POST body and reading the release id from the response. Both are
one-liners in Python, which is guaranteed-present on the Forgejo
Actions ubuntu-latest runner image. Replaced both uses; removed
the apt-get step from release.yml entirely. Slow mirrors no
longer block tagged releases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:05:21 +02:00
b96f225c3c fix(updater): /releases?limit=1 instead of /releases/latest
Some checks failed
Build ISO / build-iso (push) Successful in 17m5s
CI / lint (push) Successful in 25s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 12s
Release / release (push) Has been cancelled
Forgejo's /releases/latest silently skips pre-releases (any release
with a -alpha / -beta / -rc suffix) and 404s when there's no stable
release. During Furtka's alpha stage every tag is a pre-release, so
the Check-for-updates button always 404'd against a perfectly-valid
releases page.

Switch check_update() to GET /releases?limit=1 and take the first
entry. Forgejo returns releases newest-first regardless of kind, so
this works whether the top of the list is pre-release or stable.
Empty list (no releases published yet) now returns a clean
"no releases" UpdateError instead of a raw 404.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:29:11 +02:00
46503daf14 chore: release 26.1-alpha
All checks were successful
CI / lint (push) Successful in 25s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 12s
Release / release (push) Successful in 5m58s
2026-04-16 16:04:51 +02:00
d3c512b14f fix(furtka): point DEFAULT_BUNDLED_APPS_DIR at /opt/furtka/current/apps
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Successful in 17m5s
Third install-path miss from the versioned-layout refactor: the bundled
apps moved from /opt/furtka/apps (flat) to
/opt/furtka/versions/<ver>/apps/, reached through the /opt/furtka/current
symlink. paths.py was still pointing at the flat path, so _list_bundled
walked a non-existent directory and /api/bundled returned [] — the
fileshare tile never showed up on /apps.

Tests already use FURTKA_BUNDLED_APPS_DIR env override so nothing in
the suite needed to change. Confirmed on the VM: compat symlink
/opt/furtka/apps -> current/apps makes /api/bundled return the
fileshare manifest immediately, no service restart needed since
scanner.py reads the directory on every request.

Locking the path at current/apps rather than leaving the flat fallback
is deliberate — Phase-2 self-updates flip the symlink, and a flat
/opt/furtka/apps wouldn't move with them; bundled apps would freeze
at whatever version installed first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:58:10 +02:00
19e72cf5c3 fix(furtka): chmod 755 on version dir + heredoc furtka.json
Some checks failed
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
Two install-path bugs surfaced by SSHing into the hot-fixed test VM:

1. mktemp creates the staging dir with mode 700 by default; the rename
   to /opt/furtka/versions/<ver>/ preserved it, and Caddy (running as
   the unprivileged `caddy` user) got 403 Forbidden because it couldn't
   traverse the version dir. Now the install + self-update both
   `chmod 755` after the rename.

2. _furtka_json_cmd was a silent no-op on the 43a39a4 VM — the
   base64-encoded body + sed substitution approach layered two sets of
   quotes through archinstall's custom_commands eval, and the sed
   step either never ran or didn't match. Replaced with a plain
   heredoc that interpolates $(date -Iseconds) and $(cat VERSION) at
   chroot runtime. Result lands /var/lib/furtka/furtka.json reliably,
   which is what the landing page's hostname chip and the settings
   page's install-date field depend on.

Both issues confirmed fixed by applying them manually on the VM
(chmod 755 /opt/furtka/versions/26.0-alpha + writing furtka.json by
hand): landing page, /apps, /settings, /furtka.json all now 200 with
correct content.

Tests updated (the chmod 755 gets asserted; the old base64+sed test
gets replaced with a heredoc-shape check; the updater test asserts
0o755 mode on the finished version dir).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:46:56 +02:00
c080764c7e fix(furtka): move assets/ to repo top level so Caddy + systemd find it
All checks were successful
Build ISO / build-iso (push) Successful in 17m5s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 40s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 12s
Root cause of today's 403 on a fresh install: assets/ lived inside the
Python package at furtka/assets/, so the resource-manager tarball
extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile
has `root * /opt/furtka/current/assets/www`, systemd units point at
/opt/furtka/current/assets/bin/furtka-status, and the install-time
`systemctl link /opt/furtka/current/assets/systemd/*.service` expected
the top-level layout. All three found nothing:

- Caddy → 403 Forbidden (empty/missing document root)
- systemctl link → silent no-op, nothing ever linked into
  /etc/systemd/system/
- furtka-api.service + furtka-reconcile.service → "inactive" because
  they were never registered

Nothing in the Python package ever imported furtka.assets — these are
shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is
config data, not package data. Promoting assets/ to the repo root
matches how it's referenced everywhere downstream and eliminates the
path mismatch.

Changes:
- git mv furtka/assets assets
- iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"`
  so the tarball ships ./assets at its root, and the live-ISO copy
  reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets.
- scripts/build-release-tarball.sh: same for release tarballs.
- webinstaller/app.py: _resolve_assets_dir's dev fallback walks one
  level up to REPO_ROOT/assets/.
- tests/test_webinstaller_assets.py: ASSETS constant updated.

Tests still green (150/150) because both paths were fs-level — no
code imports changed. Next ISO build will land assets at the path
everything downstream expects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00
661f51e91a fix(iso): muzzle archinstall sync_log_to_install_medium on Py 3.14
Python 3.14 added pathlib.Path.copy() which refuses source==target with
OSError [Errno 22]. archinstall's sync_log_to_install_medium() calls
.copy() on install.log to itself at __exit__ time, because by then the
chroot mountpoint is already torn down and both source and target
resolve to the same /var/log/archinstall/install.log. The install
itself has already succeeded — the crash is in the log-sync cleanup.

Patch is a pre-start sed on the live ISO that replaces the offending
call with `None` (a no-op expression-statement keeping the same
indent level). Lives on furtka-webinstaller.service as ExecStartPre
so it runs before the first install attempt; idempotent, so service
restarts don't re-trigger anything. Never touches the installed
system — only the live ISO's site-packages tree.

Real fix is upstream in archinstall (guard the copy when source
and target resolve equal); this is a workaround until they ship it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:17:19 +02:00
a6aa95e097 docs(website): draft the two update bullets for "What works today"
All checks were successful
CI / lint (push) Successful in 27s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 13s
Held as HTML comments in both _index.md and _index.de.md so the next
site update — after the next ISO test confirms the container-image
update flow and the Furtka self-update flow on real hardware — is
just stripping the comment markers, not rewriting the copy from
scratch.

Keeping the live site honest until we can say "this works": the
Phase-1 per-app updater and the Phase-2 self-update pipeline are both
in the tree and in CI, but they haven't booted from an ISO yet. Per
the "only publish facts verifiable from repo or confirmed by user"
convention, they stay off the public page until that test is green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:30:35 +02:00
43a39a4b04 fix(webinstaller): FilesystemType.Ext4 → .EXT4 for newer archinstall
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Successful in 17m8s
archinstall's enum members got renamed from CapitalCase (Ext4) to
ALL_CAPS (EXT4) between when build_disk_config was written and the
version baked into the current Arch live ISO. Result: the install
step fires AttributeError at the archinstall JSON-dump moment,
rendering a Flask 500 right after the Confirm page.

Traceback from the VM:
  File "/opt/furtka/app.py", line 128, in build_disk_config
    filesystem_type=FilesystemType.Ext4,
AttributeError: type object 'FilesystemType' has no attribute
  'Ext4'. Did you mean: 'EXT4'?

Tests pass because build_disk_config is monkeypatched out in CI
(archinstall isn't pip-installable; it only exists on the live ISO).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:18:30 +02:00
b8fdb62b41 fix(furtka): pre-ISO audit fixes — chmod, Caddyfile refresh, unit linking
Five issues surfaced by the Phase-2 audit before the next ISO rebuild:

P1 (real blockers for a fresh install / self-update):

1. chmod +x furtka/assets/bin/furtka-status, furtka-welcome. They were
   mode 644 in git, so the tarball shipped them non-executable and every
   ExecStart referencing /opt/furtka/current/assets/bin/furtka-* would
   have failed on first boot with Permission denied.

2. apply_update now refreshes /etc/caddy/Caddyfile from the new version
   when the content differs, then reloads caddy. Without this, a release
   that changes Caddy routes silently stays on the old config.

3. apply_update now systemctl-links any new unit files shipped by the
   update, not just the five linked at install time. A future release
   that adds furtka-foo.service would otherwise never appear in
   /etc/systemd/system/.

P2 (hardening, not blockers today):

4. _resource_manager_commands now aborts the install if the tarball's
   VERSION file is empty — otherwise `mv "$staging" /opt/furtka/versions/`
   would move the staging dir in as a subdirectory and the symlink
   target would be invalid.

5. _extract_tarball passes filter='data' to tarfile.extractall on
   Python 3.12+ to catch symlink-escape / setuid / device-node tricks
   that the regex path-check can't see. Falls back silently on older
   interpreters.

Plus the CHANGELOG [Unreleased] section got filled in with the whole
Phase-1 + Phase-2 + UI-uplevel body so a 26.1-alpha tag cut off main
has meaningful release notes.

Test additions / updates:
- test_refresh_caddyfile_{copies_when_different,noops_if_source_missing}
- test_link_new_units_only_links_missing
- test_extract_tarball_uses_data_filter_when_available
- test_apply_update_happy_path now verifies the Caddyfile gets copied.
- test_resource_manager_extracts_to_versioned_slot verifies the
  empty-VERSION guard is present in the install command.

Paths now overridable via FURTKA_CADDYFILE_PATH + FURTKA_SYSTEMD_DIR so
tests can pin a tmpdir for these new fs operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:10:07 +02:00