diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae24e6b..96b03a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,31 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased]
+## [26.9-alpha] - 2026-04-21
+
+### Fixed
+
+- Landing-page app tiles with an `open_url` now open in a new tab
+ (`target="_blank" rel="noopener"`), matching the Open button
+ behaviour on `/apps`. Without this, clicking "Uptime Kuma" on the
+ home screen replaced Furtka itself with the Kuma admin page.
+ Internal links (the `Manage →` fallback for apps without an
+ `open_url`) still open in the same tab.
+- `scripts/publish-release.sh` no longer fails the whole release when
+ the ISO upload hits a Forgejo proxy 504. The core tarball + sha256 +
+ release.json (which running boxes need for self-update) are uploaded
+ first and the ISO is attempted last as a best-effort; a 504 now logs
+ a warning and exits 0 so the release page still publishes. Surfaced
+ by the 26.8-alpha cut: the tarball landed but the ~1 GB ISO upload
+ timed out at the Forgejo reverse proxy.
+
+### Changed
+
+- `furtka app list --json` now mirrors `/api/apps` field-for-field —
+ previously the CLI emitted a slim projection missing
+ `description_long`, `open_url`, and `settings`. Anyone piping the
+ CLI output into jq for automation was seeing an incomplete view.
+
## [26.8-alpha] - 2026-04-20
### Added
@@ -130,7 +155,8 @@ First tagged snapshot. Pre-alpha — the installer does not yet boot, but the de
- **Containers:** Docker + Compose
- **License:** AGPL-3.0
-[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.8-alpha...HEAD
+[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.9-alpha...HEAD
+[26.9-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.9-alpha
[26.8-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.8-alpha
[26.6-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.6-alpha
[26.5-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.5-alpha
diff --git a/assets/www/index.html b/assets/www/index.html
index 4f1513f..6bf3476 100644
--- a/assets/www/index.html
+++ b/assets/www/index.html
@@ -100,9 +100,9 @@
// frontend fall back to /apps for management.
if (app.open_url) {
const host = HOSTNAME || location.hostname;
- return { href: app.open_url.replace('{host}', host), label: 'Open' };
+ return { href: app.open_url.replace('{host}', host), label: 'Open', external: true };
}
- return { href: '/apps', label: 'Manage →' };
+ return { href: '/apps', label: 'Manage →', external: false };
}
async function renderApps() {
@@ -119,8 +119,9 @@
}
target.innerHTML = apps.map(a => {
const icon = a.icon_svg || FALLBACK_ICON;
- const { href, label } = primaryAction(a);
- return `
+ const { href, label, external } = primaryAction(a);
+ const tgt = external ? ' target="_blank" rel="noopener"' : '';
+ return `
${icon}
${esc(a.display_name || a.name)}
${esc(label)}
diff --git a/furtka/cli.py b/furtka/cli.py
index eb4452c..d4af59a 100644
--- a/furtka/cli.py
+++ b/furtka/cli.py
@@ -21,9 +21,22 @@ def _cmd_app_list(args: argparse.Namespace) -> int:
"display_name": r.manifest.display_name,
"version": r.manifest.version,
"description": r.manifest.description,
+ "description_long": r.manifest.description_long,
"volumes": list(r.manifest.volumes),
"ports": list(r.manifest.ports),
"icon": r.manifest.icon,
+ "open_url": r.manifest.open_url,
+ "settings": [
+ {
+ "name": s.name,
+ "label": s.label,
+ "description": s.description,
+ "type": s.type,
+ "required": s.required,
+ "default": s.default,
+ }
+ for s in r.manifest.settings
+ ],
}
if r.manifest
else None,
diff --git a/scripts/publish-release.sh b/scripts/publish-release.sh
index e5674a3..b784294 100755
--- a/scripts/publish-release.sh
+++ b/scripts/publish-release.sh
@@ -103,9 +103,16 @@ upload_asset "$RELEASE_JSON"
# exists. Release workflows that want this build the ISO via iso/build.sh
# and move the output here before calling publish-release. Local runs
# that skip the ISO step still publish the core release successfully.
+#
+# Soft-fail: the ISO is ~1 GB and Forgejo's reverse proxy has returned
+# 504 on the upload even when the write eventually succeeds. The core
+# tarball (which boxes need for self-update) is already uploaded above,
+# so don't let an ISO transport hiccup fail the whole release.
ISO="$DIST_DIR/furtka-$VERSION.iso"
if [ -f "$ISO" ]; then
- upload_asset "$ISO"
+ if ! upload_asset "$ISO"; then
+ echo "warning: ISO upload failed — release published without ISO asset" >&2
+ fi
fi
echo "Release $VERSION published: https://$HOST/$REPO/releases/tag/$VERSION"
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 24f8c2a..f6137b5 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -32,9 +32,21 @@ def test_app_list_json_with_one_app(tmp_path, monkeypatch, capsys):
"display_name": "Network Files",
"version": "0.1.0",
"description": "SMB",
+ "description_long": "Long description here.",
"volumes": ["files"],
"ports": [445],
"icon": "icon.svg",
+ "open_url": "smb://{host}/files",
+ "settings": [
+ {
+ "name": "SMB_USER",
+ "label": "User",
+ "description": "SMB user",
+ "type": "text",
+ "default": "furtka",
+ "required": True,
+ }
+ ],
}
)
)
@@ -43,7 +55,14 @@ def test_app_list_json_with_one_app(tmp_path, monkeypatch, capsys):
data = json.loads(capsys.readouterr().out)
assert len(data) == 1
assert data[0]["ok"] is True
- assert data[0]["manifest"]["name"] == "fileshare"
+ m = data[0]["manifest"]
+ assert m["name"] == "fileshare"
+ assert m["description_long"] == "Long description here."
+ assert m["open_url"] == "smb://{host}/files"
+ assert len(m["settings"]) == 1
+ assert m["settings"][0]["name"] == "SMB_USER"
+ assert m["settings"][0]["required"] is True
+ assert m["settings"][0]["default"] == "furtka"
def test_reconcile_dry_run_empty(tmp_path, monkeypatch, capsys):