From 5d8ac63d9f709795384a5a3442fcffa332997b1e Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Mon, 20 Apr 2026 15:44:01 +0200 Subject: [PATCH] chore: release 26.7-alpha Ships the open_url manifest field + the Open button in /apps and on the landing page, replacing the fileshare-only hardcoded deep-link with a generalised {host}-templated URL. Fileshare seed manifest bumps to 0.1.2; the furtka-apps catalog release that goes with this adds matching open_url values for fileshare + uptime-kuma. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 13 ++++++++++++- apps/fileshare/manifest.json | 3 ++- assets/www/index.html | 14 +++++++++----- assets/www/style.css | 12 +++++++++--- furtka/api.py | 12 ++++++++++++ furtka/manifest.py | 11 +++++++++++ pyproject.toml | 2 +- tests/test_manifest.py | 15 +++++++++++++++ website/content/_index.de.md | 2 +- website/content/_index.md | 2 +- website/hugo.toml | 2 +- 11 files changed, 74 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d0f0b8..7f2858b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ## [Unreleased] +## [26.7-alpha] - 2026-04-20 + +### Added + +- **Manifest `open_url` field + Open button in `/apps` and on the landing page.** Apps declare a URL template (e.g. `smb://{host}/files` for fileshare, `http://{host}:3001/` for Uptime Kuma); the UI substitutes `{host}` with the current browser's hostname at render time so the link follows however the user reached Furtka (furtka.local, raw IP, a future reverse-proxy hostname). The landing page's hardcoded `if app.name === 'fileshare'` special-case is gone — any app with an `open_url` in its manifest now gets a proper "Open" link. The core seed `apps/fileshare/manifest.json` bumps to v0.1.2 to carry it. + +### Changed + +- `.btn` CSS class introduced so an `` rendered-as-button lines up with its `` : ''} @@ -387,6 +396,9 @@ def _manifest_summary(m, app_dir=None): "icon": m.icon, "icon_svg": _read_icon_svg(app_dir, m.icon), "has_settings": bool(m.settings), + # Optional template URL with `{host}` placeholder; frontend + # substitutes against location.hostname at render time. + "open_url": m.open_url, } diff --git a/furtka/manifest.py b/furtka/manifest.py index 83006a3..f8f9f07 100644 --- a/furtka/manifest.py +++ b/furtka/manifest.py @@ -42,6 +42,12 @@ class Manifest: icon: str description_long: str = "" 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: # Namespace volume names so two apps can each declare e.g. "data" @@ -127,6 +133,10 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest: 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( name=name, display_name=str(raw["display_name"]), @@ -137,4 +147,5 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest: icon=str(raw["icon"]), description_long=str(raw.get("description_long", "")), settings=settings, + open_url=open_url_raw, ) diff --git a/pyproject.toml b/pyproject.toml index 3b80cc2..f35b65e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "furtka" -version = "26.6-alpha" +version = "26.7-alpha" description = "Open-source home server OS — simple enough for everyone." requires-python = ">=3.11" readme = "README.md" diff --git a/tests/test_manifest.py b/tests/test_manifest.py index af6f219..ef5edc4 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -95,6 +95,21 @@ def test_settings_optional_default_empty(tmp_path): m = load_manifest(path) assert m.settings == () assert m.description_long == "" + assert m.open_url == "" + + +def test_open_url_stored_when_present(tmp_path): + payload = dict(VALID_MANIFEST, open_url="smb://{host}/files") + path = _write_app(tmp_path, "fileshare", payload) + m = load_manifest(path) + assert m.open_url == "smb://{host}/files" + + +def test_open_url_non_string_rejected(tmp_path): + payload = dict(VALID_MANIFEST, open_url=42) + path = _write_app(tmp_path, "fileshare", payload) + with pytest.raises(ManifestError, match="open_url"): + load_manifest(path) def test_settings_parsed(tmp_path): diff --git a/website/content/_index.de.md b/website/content/_index.de.md index e94bbad..2a176e5 100644 --- a/website/content/_index.de.md +++ b/website/content/_index.de.md @@ -1,7 +1,7 @@ --- title: "Furtka" description: "Offenes Heimserver-Betriebssystem — einfach genug für alle." -status: "26.6-alpha — in Arbeit" +status: "26.7-alpha — in Arbeit" --- **Furtka** ist ein offenes Heimserver-Betriebssystem. diff --git a/website/content/_index.md b/website/content/_index.md index 8326b54..2d1a749 100644 --- a/website/content/_index.md +++ b/website/content/_index.md @@ -1,7 +1,7 @@ --- title: "Furtka" description: "Open-source home server OS — simple enough for everyone." -status: "26.6-alpha — work in progress" +status: "26.7-alpha — work in progress" --- **Furtka** is an open-source home server OS. diff --git a/website/hugo.toml b/website/hugo.toml index 49446a3..c505642 100644 --- a/website/hugo.toml +++ b/website/hugo.toml @@ -6,7 +6,7 @@ enableRobotsTXT = true [params] description = "Open-source home server OS — simple enough for everyone." - version = "26.6-alpha" + version = "26.7-alpha" contactEmail = "hallo@furtka.org" [markup.goldmark.renderer]