Compare commits
No commits in common. "1f2c48ee63851cc4795fd22306cfae84ddb399c0" and "6030e556d362f47dd5af8e36df7f571955220678" have entirely different histories.
1f2c48ee63
...
6030e556d3
9 changed files with 14 additions and 162 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
SIGNUPS_ALLOWED=true
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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 |
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
45
scripts/vendor/furtka_manifest.py
vendored
45
scripts/vendor/furtka_manifest.py
vendored
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue