• 26.21-alpha cdc0c3c6fd

    26.21-alpha
    All checks were successful
    Deploy site / deploy (push) Successful in 5s
    CI / lint (push) Successful in 30s
    CI / test (push) Successful in 2m55s
    CI / validate-json (push) Successful in 33s
    CI / markdown-links (push) Successful in 21s
    Build ISO / build-iso (push) Successful in 22m11s
    Release / release (push) Successful in 16m23s
    Pre-release

    daniel released this 2026-10-23 16:57:05 +02:00 | 2 commits to main since this release

    Added

    • New logo: Mark M (a lowercase f cut out of an arched gate), winner of
      the furtka.org vote (29 of 42 votes). It is now in the website header,
      favicon and share card, the installer header and tab icon, the on-box UI
      (every page and the login), the console welcome screens of the live ISO
      and the installed box, and the BIOS boot-menu splash. Brand files in
      docs/brand/, generated by website/tools/brand-assets.py.
    • furtka.org/download/ (and /de/download/): ISO download with size,
      date and checksum, requirements and step-by-step install guide. Releases
      now publish furtka-<ver>.iso.sha256 and refresh the site.

    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.

    • 32-bit UEFI machines (Atom Bay Trail / Cherry Trail) get Limine
      instead of systemd-boot: Arch has no ia32 systemd-boot and archinstall's
      GRUB path hardcodes x86_64-efi, so these boxes installed but didn't boot.

    • Force-HTTPS redirect only covers <hostname>.local / <hostname> and
      is a 302. The old 301 for every host sent devices that use the IP to a
      TLS handshake that can't succeed, and stayed cached after HTTPS was
      turned off. Self-update migrates the old snippet.

    • 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
      bench sat with furtka-welcome.timer inactive (dead) — and the console
      banner without its IP line — until the next reboot. It also linked the
      unit from /opt/furtka/versions/<ver>/… instead of /opt/furtka/current/…,
      so the link would have stopped following later updates and rollbacks
      (installer-created links use current). Now: link via current, enable,
      start.

    Changed

    • Wizard wording: "Server name" with a live http://<name>.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.
    • ISO builds pin archinstall (4.4-1, sha256-checked from the Arch archive)
      instead of taking whatever the repos ship that day; the build fails
      clearly when Arch's Python moves on. Bump steps in iso/README.md.
    • On-box UI polish: sticky top nav, bordered cards and status tiles, app
      tile hover lift, brand-red section markers; animations respect
      prefers-reduced-motion.
    Downloads
  • 26.20-alpha 8e281fa4a6

    26.20-alpha
    All checks were successful
    CI / lint (pull_request) Successful in 24s
    CI / test (pull_request) Successful in 1m19s
    CI / validate-json (pull_request) Successful in 21s
    CI / markdown-links (pull_request) Successful in 14s
    Build ISO / build-iso (push) Successful in 18m15s
    Deploy site / deploy (push) Successful in 4s
    CI / lint (push) Successful in 23s
    CI / test (push) Successful in 1m17s
    CI / validate-json (push) Successful in 21s
    CI / markdown-links (push) Successful in 13s
    Release / release (push) Successful in 12m2s
    Pre-release

    daniel released this 2026-08-25 17:12:48 +02:00 | 43 commits to main since this release

    First release shaped by a real-hardware install (old laptop mainboard, BIOS boot,
    SSD with a previous Proxmox VE on it). All four fixes are things an empty
    Proxmox VM never shows.

    Fixed

    • First catalog sync no longer dies silently at boot. On dual-stack
      LANs network-online.target is satisfied by the IPv6 SLAAC address a
      second before DHCPv4 lands, so furtka-catalog-sync.service ran into
      Network is unreachable, exited, and the next attempt was the daily timer
      — up to 6 h of jitter later. Meanwhile catalog-state.json stayed on
      checking and the apps page showed "checking…" with nothing running
      (first hardware bench, 2026-08-25). The service now retries on failure
      (every 20 s, up to 8 times in 15 min), a failed sync is recorded as stage
      error with the message, and the apps page shows "last sync failed: …"
      instead of a fake in-progress state.
    • Installer frees the target disk before partitioning. The live ISO
      auto-activates whatever an attached drive carries (LVM volume groups, md
      arrays, swap) — the first hardware bench's SSD still had a Proxmox VE
      pve VG on it — and archinstall then failed at 12 % with unable to inform the kernel of the change, probably because it/they are in use.
      install_run now runs diskprep.release_disk() first: swapoff/umount
      leaves-first, dmsetup remove / mdadm --stop for every dm/md node
      stacked on the disk, wipefs -a on partitions + disk, then
      blockdev --rereadpt. Every step is written to the top of the install
      log. The progress page also names this failure explicitly instead of a
      bare "hit a snag" if it does still happen.
    • Console banner always shows the IP fallback. On both the live ISO and
      the installed system the /etc/issue welcome was written once, after
      network-online.target, and simply omitted the http://<ip> line when no
      address existed yet — so on a slow-DHCP NIC (first hardware bench,
      2026-08-24) the screen only ever offered proksi.local. The banner script
      now runs from a timer (furtka-issue.timer on the ISO,
      furtka-welcome.timer on the installed box, every 5 s) and always prints
      a fallback line: every global IPv4 address once there is one, or a
      "no IP address yet — check the cable / DHCP" hint until then. It only
      rewrites /etc/issue and nudges agetty when the text changed, so the
      login prompt doesn't flicker. The live ISO no longer auto-logs root in
      on tty1 (releng default) — that handed the console to a shell before the
      banner could update; tty1 now stays at the login prompt with a live
      banner, and the banner tells you a shell is root + Enter away. Boxes
      that self-update get the new timer linked + enabled automatically; the
      refresh loop takes effect after their next reboot.
    Downloads
  • 26.19-alpha e1968facef

    26.19-alpha
    All checks were successful
    Build ISO / build-iso (push) Successful in 19m56s
    Deploy site / deploy (push) Successful in 4s
    CI / lint (push) Successful in 23s
    CI / test (push) Successful in 1m18s
    CI / validate-json (push) Successful in 21s
    CI / markdown-links (push) Successful in 13s
    Release / release (push) Successful in 12m36s
    Pre-release

    daniel released this 2026-08-23 16:05:43 +02:00 | 48 commits to main since this release

    Fixed

    • App installs now copy subdirectories, so dependency hooks actually
      ship.
      install_from() only copied an app's top-level files, so the
      scripts/ folder — where a provider's on_install/on_start dependency
      hooks live — never reached /var/lib/furtka/apps/<app>/, and every hooked
      dependency failed at reconcile with hook ... missing in provider. The
      whole app folder is copied now (rmtree + copytree, so a reinstall also
      drops files that were removed upstream). Found during the first real
      end-to-end run of the mosquitto + zigbee2mqtt pair on a test VM.
    • furtka app install <name> now runs on_install hooks. Named CLI
      installs copied the app folders and then ran a bare reconcile, which fires
      on_start but never on_install — so a CLI install of a consumer brought
      its provider up without ever provisioning it (no account created, empty
      MQTT_* values in the consumer's .env). Named installs now go through
      install_runner.run_install (writing the plan file first), the same docker
      phase the API dispatches, so providers come up before consumers and
      on_install hooks run. Path-based installs keep the copy + reconcile
      dev/test path.
    Downloads
  • 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 | 52 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 | 54 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 | 56 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 | 62 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 | 63 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 | 64 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 | 65 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