• 26.18-alpha 1155f1d4ba

    26.18-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 18m44s
    Deploy site / deploy (push) Successful in 4s
    CI / lint (push) Successful in 28s
    CI / test (push) Successful in 1m28s
    CI / validate-json (push) Successful in 24s
    CI / markdown-links (push) Successful in 14s
    Release / release (push) Successful in 12m5s
    Pre-release

    daniel released this 2026-06-04 16:39:04 +02:00 | 0 commits to main since this release

    Fixed

    • on_start dependency hooks now receive the consumer's stored
      credentials.
      Previously the reconciler handed an on_start hook only
      FURTKA_CONSUMER_APP/FURTKA_CONSUMER_VERSION, so it had no way to learn
      the consumer's existing secrets — which made the feature's own headline use
      case (re-create a provider account, e.g. an MQTT user, after a wipe, with
      the same password the consumer already holds) impossible without the
      provider stashing a copy itself. The hook now also gets the consumer's .env
      values, namespaced under FURTKA_CONSUMER_ENV_<KEY> (only UPPER_SNAKE_CASE
      keys, so a hand-edited .env can't produce a malformed --env argument).
      on_start stays read-only with respect to the consumer: unlike on_install,
      its stdout is intentionally not merged back into the consumer's .env — it
      reads consumer state to reconcile provider state, it doesn't mutate it.
      Surfaced by building the first real provider/consumer catalog pair
      (mosquitto + zigbee2mqtt) in daniel/furtka-apps.
    Downloads
  • 26.17-alpha b725bf1773

    26.17-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 18m3s
    CI / lint (push) Successful in 27s
    CI / test (push) Successful in 1m22s
    CI / validate-json (push) Successful in 25s
    CI / markdown-links (push) Successful in 13s
    Release / release (push) Successful in 12m13s
    Pre-release

    daniel released this 2026-05-11 20:10:04 +02:00 | 2 commits to main since this release

    Added

    • App-to-app dependencies. Manifests gain an optional requires
      array; each entry names a provider app plus two optional hook scripts
      that live in the provider's folder. on_install runs once via
      docker compose exec against the provider's running container while
      the consumer is being installed (use case: mosquitto_passwd a new
      MQTT user for the consumer). on_start runs every boot during
      reconcile, before the consumer's container starts (use case: make
      sure the user still exists after a Mosquitto wipe). Hook stdout
      parses as KEY=VALUE lines and optional FURTKA_JSON: {…} sentinel
      lines, both validated against the existing SETTING_NAME regex; the
      values get merged into the consumer's .env (hook wins on conflict)
      and the placeholder-secret check runs again over the merged file so
      a hook returning MQTT_PASS=changeme is refused the same way an
      unedited .env.example is.
    • POST /api/apps/install/plan. New read-only endpoint that
      returns the topo-sorted install order for a target app plus per-app
      summaries (display_name, version, has_settings, installed flag). The
      catalog UI calls this before opening the settings dialog so it can
      show a confirm modal — "Installing zigbee2mqtt also installs
      Mosquitto" — before anything mutates. Circular dependencies surface
      as 400 {error: "circular dependency: A -> B -> A"}; missing
      providers as 400 {error: "required app 'X' not found …"}.
    • /var/lib/furtka/install-plan.json (overridable via
      FURTKA_INSTALL_PLAN). The HTTP install endpoint writes this before
      it spawns the systemd-run background job so the runner knows the
      full chain to pull → create volumes → fire hooks → compose up for
      in plan order. The runner consumes the file after reading so a stale
      plan from a previous install can't accidentally steer the next one.

    Changed

    • furtka reconcile now visits apps in dependency order, not
      alphabetical.
      Topo-sort over requires puts providers before
      consumers so a consumer's on_start hook can talk to an already-up
      provider. Within a tier, ties stay alphabetical so boot logs are
      still deterministic across reboots. Apps with unresolvable requires
      (missing provider) are visited last; the per-app error-isolation in
      reconcile then catches them without aborting the whole sweep.
    • POST /api/apps/install requires confirm_dependencies: true
      when installing a named app would pull in transitive providers.
      Without the flag, the endpoint returns 409 plus the full plan body
      so the UI can render the confirm dialog without a second round-trip.
      Lone-target installs (no transitive deps) keep the existing
      one-click flow — no UX change for fileshare-style standalone apps.
    • furtka app install <name> and the web UI now install transitive
      dependencies automatically.
      furtka app install /path/to/dir
      stays as today (single-app, dev/test workflow).
    • compose_exec and compose_exec_script helpers in
      furtka/dockerops.py. Both pass -T (no TTY) so they work from the
      install runner and from reconcile; both raise DockerError on
      non-zero exit or timeout. compose_exec_script streams the script
      body via stdin to sh -s so hooks don't need to be baked into the
      provider's container image.

    Notes

    • Hook target service: v1 auto-picks the first service in the
      provider's compose config. Works for Mosquitto, Postgres, Redis.
      Multi-service providers (Authentik server+worker) will need an
      optional service field on the requirement entry; deferred until a
      real case lands.
    • Hook timeouts: on_install 60 s, on_start 30 s. Hardcoded for
      v1 — revisit if a DB seed legitimately needs longer.
    • Removing an app is now blocked (409 {dependents: […]} from the
      API, exit 2 from the CLI) when other installed apps require it.
    Downloads
  • 26.16-alpha 863ffa9737

    26.16-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 18m12s
    Deploy site / deploy (push) Successful in 3s
    CI / lint (push) Successful in 28s
    CI / test (push) Successful in 1m21s
    CI / validate-json (push) Successful in 24s
    CI / markdown-links (push) Successful in 13s
    Release / release (push) Successful in 12m13s
    Pre-release

    daniel released this 2026-05-10 12:59:30 +02:00 | 4 commits to main since this release

    Added

    • Failed-login rate limit on /login. A new in-memory
      LoginAttempts store in furtka/auth.py blocks brute-force attempts
      after 10 failures in 15 minutes from the same (username, IP) pair,
      with a 15-minute lockout. Successful logins clear the counter; a
      systemctl restart furtka clears any stuck lockout — fine for an
      alpha single-user box. Tuple-keying means a flood from one source IP
      can't lock the admin out from elsewhere; an attacker can rotate IPs
      to keep probing forever, but each attempt still eats the PBKDF2 cost.
      Locked attempts get a Retry-After header so the UI can render the
      cooldown.
    • Live-ISO boot USB is filtered out of the install drive picker. On
      bare-metal installs, lsblk reports the USB stick the live ISO
      booted from as TYPE=disk, so it showed up in the picker alongside
      the real install target — a user could in theory pick the USB they
      had just booted from. webinstaller/drives.py now resolves
      /run/archiso/bootmnt via findmnt, walks it up to its parent disk
      via lsblk -no PKNAME, and drops that disk before scoring. On a
      normal (non-live) box /run/archiso/bootmnt does not exist and the
      picker is unchanged.

    Changed

    • furtka.org homepage rebuild. Adopted the visual feel of Pascal's
      prototype while keeping Furtka's voice, brand palette, and bilingual
      structure: Three.js wireframe torus-knot behind the hero (color +
      opacity tied to the existing --accent CSS var so light and dark
      modes share one scene), scroll-driven camera zoom + tilt, GSAP +
      ScrollTrigger card reveals, Lenis smooth scroll, gradient wordmark,
      drop-shadow glow in dark mode, and a pulsing CTA pointing at
      /releases. "What works today" / "What's coming next" lists moved
      from markdown bullets into front-matter arrays and now render as
      scroll-reveal cards. All vendor JS (Three.js r128, GSAP 3.12.2 +
      ScrollTrigger, Lenis 1.0.33) is vendored locally under
      website/assets/js/vendor/, fingerprinted with SRI, gated to the
      homepage only, deferred so first paint isn't blocked, and
      early-returned on prefers-reduced-motion.
    • Static-asset gzip on the furtka.org nginx (config only — needs a
      deploy on forge-runner-01).
      Default nginx only gzips text/html,
      so the homepage HTML was the only asset coming back compressed. The
      ~600 KB three.min.js bundle (and the hashed CSS) were being shipped
      uncompressed across the public openresty proxy. gzip_types in
      ops/nginx/furtka.org.conf now covers css/js/json/xml/svg/woff2.
      Needs sudo ops/nginx/setup-vm.sh on forge-runner-01 to take effect
      — the site-deploy workflow only rebuilds Hugo, it doesn't touch the
      nginx config.
    Downloads
  • 26.15-alpha e68ed279cc

    26.15-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 17m23s
    CI / lint (push) Successful in 27s
    CI / test (push) Successful in 1m2s
    CI / validate-json (push) Successful in 24s
    CI / markdown-links (push) Successful in 15s
    Release / release (push) Successful in 11m34s
    Pre-release

    daniel released this 2026-04-21 19:30:04 +02:00 | 10 commits to main since this release

    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.

    Downloads
  • 26.14-alpha 26f0424ae3

    26.14-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 17m14s
    CI / lint (push) Successful in 26s
    CI / test (push) Successful in 1m2s
    CI / validate-json (push) Successful in 24s
    CI / markdown-links (push) Successful in 15s
    Release / release (push) Successful in 11m26s
    Pre-release

    daniel released this 2026-04-21 18:16:42 +02:00 | 11 commits to main since this release

    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.
    Downloads
  • 26.13-alpha 8c1fd1da2b

    26.13-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 17m28s
    CI / lint (push) Successful in 27s
    CI / test (push) Successful in 59s
    CI / validate-json (push) Successful in 23s
    CI / markdown-links (push) Successful in 15s
    Release / release (push) Successful in 11m38s
    Pre-release

    daniel released this 2026-04-21 17:03:28 +02:00 | 12 commits to main since this release

    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.
    Downloads
  • 26.12-alpha f3cd9e963c

    26.12-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 17m24s
    CI / lint (push) Successful in 26s
    CI / test (push) Successful in 43s
    CI / validate-json (push) Successful in 24s
    CI / markdown-links (push) Successful in 16s
    Release / release (push) Successful in 11m34s
    Pre-release

    daniel released this 2026-04-21 15:50:49 +02:00 | 13 commits to main since this release

    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-<app> 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 <name> (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.
    Downloads
  • 26.11-alpha 470823b347

    26.11-alpha
    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
    Pre-release

    daniel released this 2026-04-21 13:01:17 +02:00 | 14 commits to main since this release

    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.
    Downloads
  • 26.10-alpha e8c5317660

    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
    Pre-release

    daniel released this 2026-04-21 11:48:07 +02:00 | 16 commits to main since this release

    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.
    Downloads
  • 26.9-alpha c7e7c8b1e5

    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
    Pre-release

    daniel released this 2026-04-20 18:51:30 +02:00 | 20 commits to main since this release

    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.
    Downloads