Compare commits
No commits in common. "main" and "26.8-alpha" have entirely different histories.
main
...
26.8-alpha
13 changed files with 3 additions and 273 deletions
65
CHANGELOG.md
65
CHANGELOG.md
|
|
@ -6,65 +6,6 @@ Versioning: CalVer (`YY.N`) — same scheme as the core Furtka repo.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [26.12-alpha] - 2026-04-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Home Assistant** (v1.0.0, image `homeassistant/home-assistant:stable`,
|
|
||||||
daniel/furtka-apps#1). Smart-home hub for lights, sensors, and locally
|
|
||||||
controlled devices. Bridge-mode networking with explicit `8123:8123`
|
|
||||||
port mapping for v1 — Cloud integrations (Hue Cloud, Tado, Sonos via
|
|
||||||
account) work, mDNS/Bluetooth/Zigbee-stick discovery deferred until a
|
|
||||||
manifest `network_mode` knob lands. One Docker volume (`config`),
|
|
||||||
no manifest settings — onboarding (admin user, home location, units)
|
|
||||||
happens in the browser on first visit to `:8123`.
|
|
||||||
|
|
||||||
## [26.11-alpha] - 2026-04-21
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **Jellyfin compose default-substitution.** Without an `.env` in the
|
|
||||||
jellyfin app dir the CI validator's `docker compose config` step
|
|
||||||
substituted an empty `${MEDIA_PATH}` into `:/media:ro` — which
|
|
||||||
compose rejects as `empty section between colons`. Changed the
|
|
||||||
spec to `${MEDIA_PATH:-/nonexistent}:/media:ro` so the CI syntax
|
|
||||||
check always sees a valid volume even with no env file. Real
|
|
||||||
install flow (form fill → .env with user path) is unchanged; a
|
|
||||||
broken install that reaches `docker compose up` with no
|
|
||||||
MEDIA_PATH now fails loudly on a nonexistent bind target instead
|
|
||||||
of silently mounting something random.
|
|
||||||
|
|
||||||
## [26.10-alpha] - 2026-04-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Jellyfin** (v1.0.0, image `jellyfin/jellyfin:latest`). Media
|
|
||||||
server for movies, shows, music on the LAN. First app to use the new
|
|
||||||
`path`-type setting (core ≥ 26.10-alpha): the user picks their
|
|
||||||
existing media folder in the install form (`MEDIA_PATH=/mnt/media`),
|
|
||||||
the installer validates the path server-side, and docker-compose
|
|
||||||
mounts it read-only at `/media`. Docker-managed volumes handle
|
|
||||||
`/config` and `/cache`. Admin account bootstraps from the first
|
|
||||||
browser visit to port 8096. Ports: 8096. No HW transcoding yet —
|
|
||||||
that's a later schema extension.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Vendored manifest schema (`scripts/vendor/furtka_manifest.py`)
|
|
||||||
caught up to core 26.10-alpha: adds the `open_url` field (missing
|
|
||||||
since 26.6-alpha cut) and the new `path` setting type. `validate-
|
|
||||||
catalog.py` now accepts path-type settings without changes.
|
|
||||||
|
|
||||||
## [26.9-alpha] - 2026-04-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **IT-Tools** (v1.0.0, image `corentinth/it-tools:latest`). A browser
|
|
||||||
toolbox: password/UUID/QR generators, hash and HMAC, Base64 / URL /
|
|
||||||
JWT decoders, JSON/YAML/SQL formatters, regex tester, cron parser,
|
|
||||||
subnet calculator, and the usual long tail. Runs fully client-side —
|
|
||||||
no state, no volumes, no settings. Serves on port 8080.
|
|
||||||
|
|
||||||
## [26.8-alpha] - 2026-04-21
|
## [26.8-alpha] - 2026-04-21
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
@ -100,11 +41,7 @@ Versioning: CalVer (`YY.N`) — same scheme as the core Furtka repo.
|
||||||
the vendored `furtka.manifest.load_manifest` + cross-checks compose
|
the vendored `furtka.manifest.load_manifest` + cross-checks compose
|
||||||
volume references).
|
volume references).
|
||||||
|
|
||||||
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka-apps/compare/26.12-alpha...HEAD
|
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka-apps/compare/26.8-alpha...HEAD
|
||||||
[26.12-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.12-alpha
|
|
||||||
[26.11-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.11-alpha
|
|
||||||
[26.10-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.10-alpha
|
|
||||||
[26.9-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.9-alpha
|
|
||||||
[26.8-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.8-alpha
|
[26.8-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.8-alpha
|
||||||
[26.7-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.7-alpha
|
[26.7-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.7-alpha
|
||||||
[26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.6-alpha
|
[26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.6-alpha
|
||||||
|
|
|
||||||
|
|
@ -47,42 +47,10 @@ Rules enforced by `furtka/manifest.py`:
|
||||||
- `volumes` — short names, strings. Namespaced to `furtka_<app>_<short>` at runtime.
|
- `volumes` — short names, strings. Namespaced to `furtka_<app>_<short>` at runtime.
|
||||||
- `ports` — integers. Informational only; compose owns the actual port binding.
|
- `ports` — integers. Informational only; compose owns the actual port binding.
|
||||||
- `settings[].name` — must match `^[A-Z_][A-Z0-9_]*$`. This name becomes both the env-var key and the form-field ID.
|
- `settings[].name` — must match `^[A-Z_][A-Z0-9_]*$`. This name becomes both the env-var key and the form-field ID.
|
||||||
- `settings[].type` — one of `text`, `password`, `number`, `path`.
|
- `settings[].type` — one of `text`, `password`, `number`.
|
||||||
- `settings[].required` — if true, the install refuses when the value is empty.
|
- `settings[].required` — if true, the install refuses when the value is empty.
|
||||||
- `settings[].default` — optional string. Used to pre-fill the form and the bootstrapped `.env`.
|
- `settings[].default` — optional string. Used to pre-fill the form and the bootstrapped `.env`.
|
||||||
|
|
||||||
### Path-type settings (host bind mounts)
|
|
||||||
|
|
||||||
Use `"type": "path"` when the app should point at an existing folder on the host — media libraries, document archives, photo backups. The value is written to `.env` like any other setting, and compose consumes it via `${VAR}` substitution as a bind mount.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "MEDIA_PATH",
|
|
||||||
"label": "Medienordner",
|
|
||||||
"description": "Absoluter Pfad zu deinem Medien-Ordner, z.B. /mnt/media.",
|
|
||||||
"type": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
volumes:
|
|
||||||
- ${MEDIA_PATH}:/media:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
The installer (`install_from` and `update_env`) refuses values that:
|
|
||||||
|
|
||||||
- aren't absolute (must start with `/`),
|
|
||||||
- don't exist on the host,
|
|
||||||
- aren't directories,
|
|
||||||
- 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`.
|
|
||||||
|
|
||||||
Traversal like `/mnt/../etc` is caught too — the deny-list check runs on the resolved path.
|
|
||||||
|
|
||||||
Path settings sit alongside manifest-declared volumes. Use `manifest.volumes` for internal state the app owns (databases, caches, config), and path settings for user data the container should mount and — usually — read without owning. Mounting read-only (`:ro`) is a good default for data the app only consumes.
|
|
||||||
|
|
||||||
## `docker-compose.yaml`
|
## `docker-compose.yaml`
|
||||||
|
|
||||||
- File extension is `.yaml`. The compose runner hardcodes this — `.yml` will not be found.
|
- File extension is `.yaml`. The compose runner hardcodes this — `.yml` will not be found.
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
# Furtka Home Assistant — smart-home hub.
|
|
||||||
#
|
|
||||||
# Bridge networking + explicit `8123:8123` port mapping. Upstream's own
|
|
||||||
# docs lean toward `network_mode: host` because that's what mDNS-based
|
|
||||||
# discovery, Bluetooth, HomeKit, and Zigbee/Z-Wave dongles need. We're
|
|
||||||
# deliberately starting in bridge mode for v1: it keeps the catalog's
|
|
||||||
# network model consistent (every other app is bridged), and Cloud-only
|
|
||||||
# integrations (Hue Cloud, Tado, Sonos via account, etc.) work fine.
|
|
||||||
# Host networking + USB passthrough is a follow-up once the manifest
|
|
||||||
# schema grows a `network_mode` knob.
|
|
||||||
#
|
|
||||||
# Image pin: `homeassistant/home-assistant:stable` is upstream's
|
|
||||||
# recommended tag for production — release-train, not bleeding edge.
|
|
||||||
# Same image as the Docker Hub link Robert pointed at in the new-app
|
|
||||||
# request (#1).
|
|
||||||
#
|
|
||||||
# No HEALTHCHECK override needed: the upstream image's healthcheck is
|
|
||||||
# tame, and a temporarily-yellow status during the first-boot DB
|
|
||||||
# migrations is expected behaviour.
|
|
||||||
#
|
|
||||||
# No manifest.settings — Home Assistant's onboarding (admin user, home
|
|
||||||
# location, units) happens entirely in the browser on first visit, just
|
|
||||||
# like Jellyfin and Uptime Kuma. Nothing for the install form to ask.
|
|
||||||
|
|
||||||
services:
|
|
||||||
homeassistant:
|
|
||||||
image: homeassistant/home-assistant:stable
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8123:8123"
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
volumes:
|
|
||||||
- furtka_home-assistant_config:/config
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
furtka_home-assistant_config:
|
|
||||||
external: true
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M10 30 L32 10 L54 30 V52 H10 Z"/>
|
|
||||||
<circle cx="32" cy="36" r="5"/>
|
|
||||||
<path d="M32 10 V4 M22 22 L18 18 M42 22 L46 18"/>
|
|
||||||
<path d="M22 44 H42"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 341 B |
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"name": "home-assistant",
|
|
||||||
"display_name": "Home Assistant",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Smart-home hub for lights, sensors, and locally controlled devices.",
|
|
||||||
"description_long": "Steuere Lampen, Heizung, Sensoren und Smart-Home-Geräte von einem zentralen Dashboard aus. Beim ersten Aufruf im Browser unter http://furtka.local:8123 wird das Admin-Konto angelegt und die Wohnung eingerichtet. Zigbee-/Z-Wave-USB-Sticks und automatische Geräteerkennung über mDNS/Bluetooth sind in dieser ersten Version noch nicht verdrahtet — die App läuft im Bridge-Modus auf Port 8123. Cloud-Integrationen (Hue Cloud, Tado, Sonos via Account) funktionieren ohne Einschränkung.",
|
|
||||||
"volumes": ["config"],
|
|
||||||
"ports": [8123],
|
|
||||||
"icon": "icon.svg",
|
|
||||||
"open_url": "http://{host}:8123/"
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# Furtka IT-Tools — browser-side utility toolbox.
|
|
||||||
#
|
|
||||||
# IT-Tools is a purely static single-page app: the container serves
|
|
||||||
# prebuilt HTML/JS and does all work client-side. No state, no volumes,
|
|
||||||
# no env knobs — that's why this app has no manifest.settings and no
|
|
||||||
# .env.example.
|
|
||||||
#
|
|
||||||
# TODO(image-pin): `:latest` is shaky for production — pin to a digest
|
|
||||||
# (`corentinth/it-tools@sha256:...`) or a stable tag once we've verified
|
|
||||||
# one against the upstream registry. For the MVP run we accept the drift
|
|
||||||
# risk to keep the install reproducible against whatever the upstream
|
|
||||||
# image happens to be on test day; revisit before any non-developer
|
|
||||||
# touches this.
|
|
||||||
|
|
||||||
services:
|
|
||||||
it-tools:
|
|
||||||
image: corentinth/it-tools:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="32" cy="32" r="26" opacity="0.28"/>
|
|
||||||
<path d="M42 14 L50 22 L44 28 L38 22 Z M40 26 L22 44 L18 48 L14 44 L18 40 L36 22"/>
|
|
||||||
<path d="M16 50 L20 46"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 349 B |
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"name": "it-tools",
|
|
||||||
"display_name": "IT-Tools",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Collection of small developer and admin utilities in one web UI.",
|
|
||||||
"description_long": "Werkzeugkasten für den Browser: Passwörter generieren, Hashes berechnen, JSON/YAML formatieren, Base64 en- und dekodieren, JWTs lesen, Regex testen, UUIDs oder QR-Codes erzeugen und vieles mehr. Läuft komplett im Browser — es werden keine Daten an externe Dienste gesendet.",
|
|
||||||
"volumes": [],
|
|
||||||
"ports": [8080],
|
|
||||||
"icon": "icon.svg",
|
|
||||||
"open_url": "http://{host}:8080/"
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
MEDIA_PATH=
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# Furtka Jellyfin — media server.
|
|
||||||
#
|
|
||||||
# Two Docker-managed volumes (config, cache) for app state + one
|
|
||||||
# user-supplied host path (MEDIA_PATH) mounted read-only for the media
|
|
||||||
# library. Admin account bootstraps from the first browser visit to
|
|
||||||
# :8096 — that's why this app has no manifest.settings for admin creds.
|
|
||||||
#
|
|
||||||
# MEDIA_PATH is a `path`-type setting (furtka/manifest.py ≥ 26.10-alpha
|
|
||||||
# schema). The install form asks for it, the installer validates that
|
|
||||||
# the directory exists and isn't a system path, and docker-compose
|
|
||||||
# substitutes the value below at `docker compose up` time.
|
|
||||||
#
|
|
||||||
# The `${MEDIA_PATH:-/nonexistent}` default-substitution keeps
|
|
||||||
# `validate-catalog.py` (which runs `docker compose config` without any
|
|
||||||
# .env) from failing on the empty-string case: an empty MEDIA_PATH
|
|
||||||
# would expand to `:/media:ro` which compose rejects as "empty section
|
|
||||||
# between colons". /nonexistent is an obviously-wrong fallback so if it
|
|
||||||
# ever actually reaches `docker compose up` (which requires a broken
|
|
||||||
# install flow), the mount fails loudly instead of silently mounting
|
|
||||||
# something random.
|
|
||||||
#
|
|
||||||
# TODO(image-pin): `:latest` is shaky for production — pin to a digest
|
|
||||||
# (`jellyfin/jellyfin@sha256:...`) or a stable tag once we've verified
|
|
||||||
# one against the upstream registry. MVP drift risk accepted.
|
|
||||||
#
|
|
||||||
# No HW transcoding yet — /dev/dri passthrough is a separate, later
|
|
||||||
# schema extension. 1080p software transcode + Direct Play over LAN
|
|
||||||
# are fine for the Medion-Haswell target.
|
|
||||||
|
|
||||||
services:
|
|
||||||
jellyfin:
|
|
||||||
image: jellyfin/jellyfin:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8096:8096"
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
volumes:
|
|
||||||
- furtka_jellyfin_config:/config
|
|
||||||
- furtka_jellyfin_cache:/cache
|
|
||||||
- ${MEDIA_PATH:-/nonexistent}:/media:ro
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
furtka_jellyfin_config:
|
|
||||||
external: true
|
|
||||||
furtka_jellyfin_cache:
|
|
||||||
external: true
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="32" cy="32" r="26" opacity="0.28"/>
|
|
||||||
<path d="M12 18 H52 V46 H12 Z M12 26 H52 M18 22 L18 22 M24 22 L24 22 M30 22 L30 22"/>
|
|
||||||
<path d="M28 32 L28 40 L38 36 Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 360 B |
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "jellyfin",
|
|
||||||
"display_name": "Jellyfin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Media server for movies, shows, and music on the LAN.",
|
|
||||||
"description_long": "Ein eigener Streaming-Server für deine Filme, Serien und Musik. Apps für Smart-TVs, Handy und Web-Browser verfügbar. Admin-Konto wird beim ersten Besuch im Browser angelegt. Medien werden nur gelesen, nicht verändert.",
|
|
||||||
"volumes": ["config", "cache"],
|
|
||||||
"ports": [8096],
|
|
||||||
"icon": "icon.svg",
|
|
||||||
"open_url": "http://{host}:8096/",
|
|
||||||
"settings": [
|
|
||||||
{
|
|
||||||
"name": "MEDIA_PATH",
|
|
||||||
"label": "Medienordner",
|
|
||||||
"description": "Absoluter Pfad zu deinem Filme-/Serien-Ordner auf dieser Maschine, z.B. /mnt/media. Wird nur gelesen.",
|
|
||||||
"type": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
13
scripts/vendor/furtka_manifest.py
vendored
13
scripts/vendor/furtka_manifest.py
vendored
|
|
@ -13,7 +13,7 @@ REQUIRED_FIELDS = (
|
||||||
"icon",
|
"icon",
|
||||||
)
|
)
|
||||||
|
|
||||||
VALID_SETTING_TYPES = frozenset({"text", "password", "number", "path"})
|
VALID_SETTING_TYPES = frozenset({"text", "password", "number"})
|
||||||
SETTING_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
SETTING_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,12 +42,6 @@ class Manifest:
|
||||||
icon: str
|
icon: str
|
||||||
description_long: str = ""
|
description_long: str = ""
|
||||||
settings: tuple[Setting, ...] = field(default_factory=tuple)
|
settings: tuple[Setting, ...] = field(default_factory=tuple)
|
||||||
# Optional "Open" link for the landing page + installed-app row.
|
|
||||||
# `{host}` is substituted with the current browser hostname at render
|
|
||||||
# time so the URL follows whatever the user typed to reach Furtka —
|
|
||||||
# furtka.local, a raw IP, a future reverse-proxy hostname. Apps with
|
|
||||||
# no frontend (CLI-only, background workers) leave this empty.
|
|
||||||
open_url: str = ""
|
|
||||||
|
|
||||||
def volume_name(self, short: str) -> str:
|
def volume_name(self, short: str) -> str:
|
||||||
# Namespace volume names so two apps can each declare e.g. "data"
|
# Namespace volume names so two apps can each declare e.g. "data"
|
||||||
|
|
@ -133,10 +127,6 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
|
||||||
|
|
||||||
settings = _parse_settings(raw.get("settings"), path)
|
settings = _parse_settings(raw.get("settings"), path)
|
||||||
|
|
||||||
open_url_raw = raw.get("open_url", "")
|
|
||||||
if not isinstance(open_url_raw, str):
|
|
||||||
raise ManifestError(f"{path}: open_url must be a string if set")
|
|
||||||
|
|
||||||
return Manifest(
|
return Manifest(
|
||||||
name=name,
|
name=name,
|
||||||
display_name=str(raw["display_name"]),
|
display_name=str(raw["display_name"]),
|
||||||
|
|
@ -147,5 +137,4 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
|
||||||
icon=str(raw["icon"]),
|
icon=str(raw["icon"]),
|
||||||
description_long=str(raw.get("description_long", "")),
|
description_long=str(raw.get("description_long", "")),
|
||||||
settings=settings,
|
settings=settings,
|
||||||
open_url=open_url_raw,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue