furtka/docs/resource-manager.md
Daniel Maksymilian Syrnicki 8498dd576f fix(furtka): rename "Einstellungen" button to "Settings"
Leftover German string from prototyping — the rest of the apps UI is
English, so it stood out as a mixed-language bug during 2026-04-16
VM testing.

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

8.9 KiB

Resource Manager

The layer between Furtka apps and the underlying system (disk, Docker, network). Apps don't touch Docker or the filesystem directly — they declare what they need in a manifest and the Resource Manager provisions, runs, and tracks them.

Status: v1 shipped 2026-04-15 in commits cfc4c0b..c6ed7a8, validated end-to-end on Proxmox VM same day (install → Web UI → fileshare install → SMB client → reboot-persistence all green). Commit 61c7ee2 adds the in-browser settings form and description_long so users no longer need SSH to configure an app. First consumer is the LAN fileshare app at apps/fileshare/. Web UI at http://<host>.local/apps.

For the conversation that produced these decisions and the Q&A live in the chat, see ~/.claude/plans/stateful-juggling-pike.md.


Anatomy of an app

Every Furtka app is a directory containing exactly:

manifest.json       # required — the contract
docker-compose.yaml # required — container lifecycle
.env.example        # optional — bootstraps .env on first install
.env                # optional — user-edited secrets (preserved across upgrades)
icon.svg            # optional but referenced in the manifest

The directory name is the app name. The manifest's name field must match it once installed (the scanner enforces this). When you install from an arbitrary source path the manifest's name decides where it lands — so furtka app install /tmp/some-fork/ works regardless of what the source folder is called.

Manifest schema

JSON. Fields marked required are mandatory; optional ones default as noted.

{
  "name": "fileshare",
  "display_name": "Network Files",
  "version": "0.1.0",
  "description": "SMB share for LAN devices",
  "description_long": "Longer user-facing help shown above the settings form.",
  "volumes": ["files"],
  "ports": [445, 139],
  "icon": "icon.svg",
  "settings": [
    {
      "name": "SMB_USER",
      "label": "Benutzername",
      "description": "Der Name, mit dem sich Geräte am Share anmelden.",
      "type": "text",
      "default": "furtka",
      "required": true
    },
    {
      "name": "SMB_PASSWORD",
      "label": "Passwort",
      "description": "Mindestens 8 Zeichen. Wird nie angezeigt.",
      "type": "password",
      "required": true
    }
  ]
}

Top-level fields:

  • name (required) — machine id, must equal the install folder name.
  • display_name (required) — shown in the UI.
  • version (required) — free-form string (semver expected, not enforced).
  • description (required) — one-line summary rendered on the card.
  • description_long (optional, default "") — multi-sentence help text rendered above the settings form. Plain text, newlines preserved.
  • volumes (required) — list of short names. Furtka creates each as furtka_<app>_<vol> (collision-free across apps). Compose files MUST reference the namespaced name as external: true.
  • ports (required) — informational for the UI. Compose owns the actual port binding.
  • icon (required) — relative path inside the app folder.
  • settings (optional, default []) — see next section.

Settings schema

Each entry in settings declares one environment variable the user can fill in via the Web UI (or via POST /api/apps/install with a settings object). On install/edit the values are written to .env in the app folder.

  • name (required) — env-var name. Must match ^[A-Z_][A-Z0-9_]*$ (UPPER_SNAKE_CASE). Duplicates rejected.
  • label (optional, defaults to name) — human-readable label rendered as the form label.
  • description (optional, default "") — one-sentence help text rendered under the label.
  • type (optional, default "text") — one of "text", "password", "number". Controls the HTML input type AND whether the current value is masked on the settings GET endpoint (password values are never returned, only written).
  • required (optional, default false) — checked against the merged .env after submit, so edit-mode can omit unchanged fields and still pass.
  • default (optional, default null) — applied on first install if the user didn't submit the field. String-coerced if non-string.

Merge semantics on edit: the installed app's existing .env is the base, submitted settings overlay it. Omit a field and its current value is preserved; submit "" and it's cleared (which triggers required rejection).

Placeholder rejection still applies: if a final .env value matches furtka.installer.PLACEHOLDER_SECRETS (currently {"changeme"}), install fails with InstallError — so apps shipping an .env.example with changeme stay safe even if the user skips the form.


Lifecycle

Discovery: boot-scan

furtka-reconcile.service (oneshot, after docker.service) runs furtka reconcile at every boot. The reconciler walks /var/lib/furtka/apps/*, validates each manifest, ensures every declared volume exists, then docker compose up -d per app. Filesystem is the only source of truth — no separate index, no DB.

A failed reconcile of one app does not abort the others. The CLI exits non-zero if any app errored, so systemd marks the unit red, but the healthy apps still come up.

Install

  • furtka app install <path> — install from a local folder.
  • furtka app install <name> — falls back to /opt/furtka/apps/<name>/ (apps bundled with the ISO).
  • Web UI: click Install on a card under "Available to install".

The installer copies files into /var/lib/furtka/apps/<name>/, preserves any existing user .env, bootstraps .env from .env.example on first install, and chmod 0600 on .env.

Placeholder secrets are refused. If .env ends up containing values listed in furtka.installer.PLACEHOLDER_SECRETS (currently {"changeme"}), install raises InstallError and the reconciler is not run. Files are left in place so the user can vim the .env and re-run install.

Remove

  • furtka app remove <name>docker compose down, then delete the app folder.
  • Web UI: Remove button.

Volumes are NEVER deleted. Reinstall recovers the data. Manual docker volume rm furtka_<app>_<vol> if you really want to wipe.


Backend

furtka/api.py runs as furtka-api.service — Python stdlib http.server (no Flask), bound to 127.0.0.1:7000. Caddy reverse-proxies /api/* and /apps* from :80.

Endpoints:

  • GET / and /apps — self-contained HTML UI.
  • GET /api/apps — installed apps as JSON (each includes has_settings so the UI can show the "Settings" button only when relevant).
  • GET /api/bundled — apps available in /opt/furtka/apps/ that aren't installed.
  • GET /api/apps/<name>/settings — returns the manifest's settings alongside current .env values. Works for both installed and bundled apps. Password values are returned as empty strings.
  • POST /api/apps/<name>/settings {"settings": {...}} — merges values into the installed app's .env and reconciles. Only for already-installed apps.
  • POST /api/apps/install {"name": "...", "settings": {...}} — install/reinstall. settings is optional; if present the .env is written from it before the placeholder check.
  • POST /api/apps/remove {"name": "..."} — remove (folder, not volume).

The UI has no authentication. It shouts the warning at the top. Authentik integration is the proper fix later.


Out of scope for v1

These are deliberate omissions, not forgotten work. Adding any of them is a discussed design change.

  • SQL database — filesystem is authoritative, full stop.
  • Volume sharing between apps (would be the first DB use case).
  • Auth on the web UI.
  • TLS on .local (separate problem — see commit history around mDNS for the reasoning).
  • Catalog repo — install <name> only resolves bundled apps, no network catalog.
  • Auto-updates of installed apps.
  • Free-form .env editor — the settings form only exposes fields declared in the manifest. Non-manifest keys in .env are preserved on edit but not editable through the UI.

Code map

File Purpose
furtka/manifest.py JSON schema validation, Manifest dataclass, namespacing helper
furtka/scanner.py Walks /var/lib/furtka/apps/, returns ScanResults (broken manifests = error, not exception)
furtka/reconciler.py Drives the per-app loop; isolates errors so one broken app doesn't block others
furtka/installer.py Copy-from-source, .env bootstrap + 0600, placeholder rejection
furtka/dockerops.py docker volume + docker compose subprocess wrappers
furtka/api.py HTTP server + HTML UI
furtka/cli.py furtka app list/install/remove, furtka reconcile, furtka serve
apps/fileshare/ First consumer — SMB share via dperson/samba
iso/build.sh Tarballs furtka/ + apps/ into the live ISO at build time
webinstaller/app.py _resource_manager_commands() + new systemd units (reconcile + api) for archinstall custom_commands