Compare commits

..

No commits in common. "1f2c48ee63851cc4795fd22306cfae84ddb399c0" and "6030e556d362f47dd5af8e36df7f571955220678" have entirely different histories.

9 changed files with 14 additions and 162 deletions

View file

@ -6,28 +6,6 @@ Versioning: CalVer (`YY.N`) — same scheme as the core Furtka repo.
## [Unreleased] ## [Unreleased]
## [26.14-alpha] - 2026-10-23
### Added
- **Vaultwarden** (v1.0.0, image `vaultwarden/server:1.37.4`, multi-arch).
Bitwarden-compatible password manager on port 8222, one Docker volume
(`data`). Install form asks for `SIGNUPS_ALLOWED` (default `true`; turn it
off after creating your accounts). The web vault and clients need HTTPS, so
use it through Furtka Gateway or the box's local HTTPS. `/admin` stays
disabled (no `ADMIN_TOKEN`). Declares `internet.viable` on 8222.
- **Gateway-ready apps:** `it-tools` (1.1.0, port 8080), `uptime-kuma`
(1.1.0, 3001) and `jellyfin` (1.1.0, 8096) declare
`"internet": {"viable": true, "port": …}`, so core 26.21-alpha can expose
them through Furtka Gateway (Apps → ⋯ → Expose to Internet). All three have
their own login or hold no data. Not exposable on purpose: fileshare (SMB),
mosquitto, zigbee2mqtt, and home-assistant (needs `trusted_proxies` first).
### Changed
- `scripts/vendor/furtka_manifest.py` synced with core 26.21-alpha (adds the
`internet` block).
## [26.13-alpha] - 2026-08-23 ## [26.13-alpha] - 2026-08-23
### Added ### Added
@ -164,8 +142,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.14-alpha...HEAD [Unreleased]: https://forgejo.sourcegate.online/daniel/furtka-apps/compare/26.13-alpha...HEAD
[26.14-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.14-alpha
[26.13-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.13-alpha [26.13-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.13-alpha
[26.12-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.12-alpha [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.11-alpha]: https://forgejo.sourcegate.online/daniel/furtka-apps/releases/tag/26.11-alpha

View file

@ -1,17 +1,11 @@
{ {
"name": "it-tools", "name": "it-tools",
"display_name": "IT-Tools", "display_name": "IT-Tools",
"version": "1.1.0", "version": "1.0.0",
"description": "Collection of small developer and admin utilities in one web UI.", "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.", "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": [], "volumes": [],
"ports": [ "ports": [8080],
8080
],
"icon": "icon.svg", "icon": "icon.svg",
"open_url": "http://{host}:8080/", "open_url": "http://{host}:8080/"
"internet": {
"viable": true,
"port": 8080
}
} }

View file

@ -1,16 +1,11 @@
{ {
"name": "jellyfin", "name": "jellyfin",
"display_name": "Jellyfin", "display_name": "Jellyfin",
"version": "1.1.0", "version": "1.0.0",
"description": "Media server for movies, shows, and music on the LAN.", "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.", "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": [ "volumes": ["config", "cache"],
"config", "ports": [8096],
"cache"
],
"ports": [
8096
],
"icon": "icon.svg", "icon": "icon.svg",
"open_url": "http://{host}:8096/", "open_url": "http://{host}:8096/",
"settings": [ "settings": [
@ -21,9 +16,5 @@
"type": "path", "type": "path",
"required": true "required": true
} }
], ]
"internet": {
"viable": true,
"port": 8096
}
} }

View file

@ -1,19 +1,11 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"display_name": "Uptime Kuma", "display_name": "Uptime Kuma",
"version": "1.1.0", "version": "1.0.1",
"description": "Self-hosted status monitor for services, ports, and devices on the LAN.", "description": "Self-hosted status monitor for services, ports, and devices on the LAN.",
"description_long": "Überwacht andere Geräte und Dienste im Netzwerk und schickt dir Bescheid, wenn etwas ausfällt. Nach der Installation den Admin-Account direkt im Browser unter http://furtka.local:3001 anlegen, dann Monitore (Ping, Webseite, Port, Docker-Container) hinzufügen.", "description_long": "Überwacht andere Geräte und Dienste im Netzwerk und schickt dir Bescheid, wenn etwas ausfällt. Nach der Installation den Admin-Account direkt im Browser unter http://furtka.local:3001 anlegen, dann Monitore (Ping, Webseite, Port, Docker-Container) hinzufügen.",
"volumes": [ "volumes": ["data"],
"data" "ports": [3001],
],
"ports": [
3001
],
"icon": "icon.svg", "icon": "icon.svg",
"open_url": "http://{host}:3001/", "open_url": "http://{host}:3001/"
"internet": {
"viable": true,
"port": 3001
}
} }

View file

@ -1 +0,0 @@
SIGNUPS_ALLOWED=true

View file

@ -1,32 +0,0 @@
# Furtka Vaultwarden — Bitwarden-compatible password manager.
#
# One Docker-managed volume (data) holds the SQLite vault, attachments and
# the RSA keys. The volume `furtka_vaultwarden_data` is created by the Furtka
# reconciler from the manifest's "volumes" list before compose comes up;
# `external: true` keeps compose from managing its lifecycle.
#
# HTTPS: the Bitwarden web vault and clients need a secure context, so plain
# http://<host>:8222 only shows a warning page. Reach it through Furtka
# Gateway (manifest `internet.viable`) or the box's local HTTPS.
#
# No ADMIN_TOKEN: the /admin panel stays disabled. Account creation is
# controlled by SIGNUPS_ALLOWED from the install form; the user turns it off
# after creating their accounts.
#
# Image pin: 1.37.4 (multi-arch, x86_64 + arm64). Bump deliberately.
services:
vaultwarden:
image: vaultwarden/server:1.37.4
restart: unless-stopped
ports:
- "8222:80"
environment:
- TZ=Europe/Berlin
- SIGNUPS_ALLOWED=${SIGNUPS_ALLOWED:-true}
volumes:
- furtka_vaultwarden_data:/data
volumes:
furtka_vaultwarden_data:
external: true

View file

@ -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="M32 6 L54 14 V30 C54 44 44 54 32 58 C20 54 10 44 10 30 V14 Z" opacity="0.28"/>
<rect x="22" y="28" width="20" height="16" rx="3"/>
<path d="M26 28 V23 a6 6 0 0 1 12 0 V28"/>
<path d="M32 34 V38"/>
</svg>

Before

Width:  |  Height:  |  Size: 399 B

View file

@ -1,22 +0,0 @@
{
"name": "vaultwarden",
"display_name": "Vaultwarden",
"version": "1.0.0",
"description": "Password manager compatible with the Bitwarden apps and browser extensions.",
"description_long": "Dein eigener Passwortmanager, kompatibel mit den offiziellen Bitwarden-Apps für Handy, Browser und PC. Wichtig: Der Web-Tresor und die Apps brauchen HTTPS. Am einfachsten über Furtka Gateway (Apps → ⋯ → Expose to Internet) oder mit eingeschaltetem lokalem HTTPS. Erstes Konto im Browser anlegen, danach in den Einstellungen \"Neue Konten erlauben\" auf false stellen, damit sich niemand Fremdes registriert.",
"volumes": ["data"],
"ports": [8222],
"icon": "icon.svg",
"open_url": "http://{host}:8222/",
"internet": { "viable": true, "port": 8222 },
"settings": [
{
"name": "SIGNUPS_ALLOWED",
"label": "Neue Konten erlauben",
"description": "true, solange du dein Konto (und das deiner Familie) anlegst. Danach auf false setzen, besonders wenn Vaultwarden über das Gateway aus dem Internet erreichbar ist.",
"type": "text",
"required": true,
"default": "true"
}
]
}

View file

@ -32,21 +32,6 @@ class Setting:
default: str | None default: str | None
@dataclass(frozen=True)
class Internet:
# Whether this app is safe to expose to the internet through a
# furtka-gateway at all. Only ever a catalog author's self-declaration —
# the gateway independently re-validates the port it's given at publish
# time regardless of what this says (see furtka-gateway's routes.py).
viable: bool
# Which declared `ports` entry is the exposable web UI. Required when
# viable is True — an app declaring several ports (e.g. a DNS service
# exposing both its resolver port and an admin UI port) must say which
# one is actually meant to be reachable from the internet; there is no
# way to infer that from `ports` alone.
port: int | None
@dataclass(frozen=True) @dataclass(frozen=True)
class Requirement: class Requirement:
app: str # name of the required app — must resolve in installed/catalog/bundled app: str # name of the required app — must resolve in installed/catalog/bundled
@ -77,7 +62,6 @@ class Manifest:
# no frontend (CLI-only, background workers) leave this empty. # no frontend (CLI-only, background workers) leave this empty.
open_url: str = "" open_url: str = ""
requires: tuple[Requirement, ...] = field(default_factory=tuple) requires: tuple[Requirement, ...] = field(default_factory=tuple)
internet: Internet = field(default_factory=lambda: Internet(viable=False, port=None))
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"
@ -171,27 +155,6 @@ def _parse_requires(raw: object, manifest_path: Path, self_name: str) -> tuple[R
return tuple(out) return tuple(out)
def _parse_internet(raw: object, manifest_path: Path, ports: list[int]) -> Internet:
if raw is None:
return Internet(viable=False, port=None)
if not isinstance(raw, dict):
raise ManifestError(f"{manifest_path}: internet must be an object")
viable = bool(raw.get("viable", False))
port = raw.get("port")
if not viable:
return Internet(viable=False, port=None)
if not isinstance(port, int) or isinstance(port, bool):
raise ManifestError(f"{manifest_path}: internet.port must be an integer when viable")
if port not in ports:
raise ManifestError(
f"{manifest_path}: internet.port {port} must be one of the declared ports {ports}"
)
return Internet(viable=True, port=port)
def load_manifest(path: Path, expected_name: str | None = None) -> Manifest: def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
"""Parse and validate a manifest.json. """Parse and validate a manifest.json.
@ -212,10 +175,8 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
raise ManifestError(f"{path}: missing required fields: {', '.join(missing)}") raise ManifestError(f"{path}: missing required fields: {', '.join(missing)}")
name = raw["name"] name = raw["name"]
if not isinstance(name, str) or not APP_NAME_RE.fullmatch(name): if not isinstance(name, str) or not name:
raise ManifestError( raise ManifestError(f"{path}: name must be a non-empty string")
f"{path}: name must be lowercase letters, digits, '-' or '_' (got {name!r})"
)
if expected_name is not None and name != expected_name: if expected_name is not None and name != expected_name:
raise ManifestError(f"{path}: name {name!r} must equal {expected_name!r}") raise ManifestError(f"{path}: name {name!r} must equal {expected_name!r}")
@ -229,7 +190,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)
requires = _parse_requires(raw.get("requires"), path, name) requires = _parse_requires(raw.get("requires"), path, name)
internet = _parse_internet(raw.get("internet"), path, ports)
open_url_raw = raw.get("open_url", "") open_url_raw = raw.get("open_url", "")
if not isinstance(open_url_raw, str): if not isinstance(open_url_raw, str):
@ -247,5 +207,4 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
settings=settings, settings=settings,
open_url=open_url_raw, open_url=open_url_raw,
requires=requires, requires=requires,
internet=internet,
) )