chore: release 26.7-alpha
Some checks failed
Deploy site / deploy (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
CI / lint (push) Successful in 1m26s
CI / test (push) Successful in 1m18s
CI / validate-json (push) Successful in 52s
CI / markdown-links (push) Successful in 27s
Release / release (push) Has been cancelled
Some checks failed
Deploy site / deploy (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
CI / lint (push) Successful in 1m26s
CI / test (push) Successful in 1m18s
CI / validate-json (push) Successful in 52s
CI / markdown-links (push) Successful in 27s
Release / release (push) Has been cancelled
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) <noreply@anthropic.com>
This commit is contained in:
parent
018f2e20b0
commit
5d8ac63d9f
11 changed files with 74 additions and 14 deletions
13
CHANGELOG.md
13
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 `<a>` rendered-as-button lines up with its `<button>` siblings in `.buttons`. Needed because "Open" is a real link (middle-click, copy URL, screen readers) and HTML doesn't let `<button>` carry `href`.
|
||||
|
||||
## [26.6-alpha] - 2026-04-20
|
||||
|
||||
### Added
|
||||
|
|
@ -114,7 +124,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.6-alpha...HEAD
|
||||
[Unreleased]: https://forgejo.sourcegate.online/daniel/furtka/compare/26.7-alpha...HEAD
|
||||
[26.7-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.7-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
|
||||
[26.4-alpha]: https://forgejo.sourcegate.online/daniel/furtka/releases/tag/26.4-alpha
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "fileshare",
|
||||
"display_name": "Network Files",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "SMB share for Mac, Windows, Linux and Android devices on the LAN.",
|
||||
"description_long": "Alle Geräte im WLAN sehen einen gemeinsamen Ordner. Funktioniert mit Windows, Mac, Linux und Android. Verbinden zu smb://furtka.local — Anmeldung mit dem hier gesetzten Benutzernamen und Passwort.",
|
||||
"volumes": ["files"],
|
||||
"ports": [445, 139],
|
||||
"icon": "icon.svg",
|
||||
"open_url": "smb://{host}/files",
|
||||
"settings": [
|
||||
{
|
||||
"name": "SMB_USER",
|
||||
|
|
|
|||
|
|
@ -92,11 +92,15 @@
|
|||
}
|
||||
|
||||
function primaryAction(app) {
|
||||
// Only fileshare has a direct "open" link today. Future apps with
|
||||
// HTTP endpoints would surface a URL here; everything else falls
|
||||
// back to the /apps manage page.
|
||||
if (app.name === 'fileshare' && HOSTNAME) {
|
||||
return { href: `smb://${HOSTNAME}.local/files`, label: 'Open files' };
|
||||
// open_url is a manifest-declared template with a `{host}`
|
||||
// placeholder — substituted against the current browser's
|
||||
// hostname so smb://host/files and http://host:3001/ both
|
||||
// follow however the user reached Furtka (furtka.local, raw
|
||||
// IP, a future reverse-proxy hostname). Apps without a
|
||||
// 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: '/apps', label: 'Manage →' };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ h2 {
|
|||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
button {
|
||||
button, .btn {
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: var(--bg);
|
||||
|
|
@ -209,15 +209,21 @@ button {
|
|||
white-space: nowrap;
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
/* Anchor rendered-as-button: strip underline + keep the button's
|
||||
rectangular hit area. `display: inline-flex` so an <a class="btn">
|
||||
lines up vertically with its <button> siblings in .buttons. */
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
button.secondary {
|
||||
button.secondary, .btn.secondary {
|
||||
background: var(--card);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
button.danger { background: var(--danger); color: #fff; }
|
||||
button:disabled { opacity: 0.5; cursor: wait; }
|
||||
button:focus-visible { outline: none; box-shadow: var(--ring); }
|
||||
button:focus-visible, .btn:focus-visible { outline: none; box-shadow: var(--ring); }
|
||||
.empty { color: var(--muted); font-style: italic; padding: 0.5rem 0; }
|
||||
.catalog-row {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -248,6 +248,14 @@ async function refresh() {
|
|||
document.getElementById('installed').innerHTML = installed.length
|
||||
? installed.map(a => {
|
||||
const hasSettings = a.has_settings;
|
||||
const openHref = a.open_url ? a.open_url.replace('{host}', location.hostname) : '';
|
||||
// Plain <a> rendered as a button so it behaves like a real link
|
||||
// (middle-click, right-click "copy link", screen readers) instead
|
||||
// of a JS onclick. Most installed apps will want this — fileshare
|
||||
// deep-links to smb://, Kuma to http://host:3001/.
|
||||
const openBtn = openHref
|
||||
? `<a class="btn" href="${esc(openHref)}" target="_blank" rel="noopener">Open</a>`
|
||||
: '';
|
||||
return `
|
||||
<div class="app">
|
||||
<div class="left">
|
||||
|
|
@ -258,6 +266,7 @@ async function refresh() {
|
|||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
${openBtn}
|
||||
${hasSettings ? `<button data-op="edit" data-name="${esc(a.name)}">Settings</button>` : ''}
|
||||
<button class="secondary" data-op="update" data-name="${esc(a.name)}">Update</button>
|
||||
<button class="secondary" data-op="reinstall" data-name="${esc(a.name)}">Reinstall</button>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "Furtka"
|
||||
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
|
||||
status: "<span class=\"mono\">26.6-alpha</span> — in Arbeit"
|
||||
status: "<span class=\"mono\">26.7-alpha</span> — in Arbeit"
|
||||
---
|
||||
|
||||
**Furtka** ist ein offenes Heimserver-Betriebssystem.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "Furtka"
|
||||
description: "Open-source home server OS — simple enough for everyone."
|
||||
status: "<span class=\"mono\">26.6-alpha</span> — work in progress"
|
||||
status: "<span class=\"mono\">26.7-alpha</span> — work in progress"
|
||||
---
|
||||
|
||||
**Furtka** is an open-source home server OS.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue