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>
156 lines
8.9 KiB
Markdown
156 lines
8.9 KiB
Markdown
# 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.
|
|
|
|
```json
|
|
{
|
|
"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 `ScanResult`s (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 |
|