Compare commits

...

3 commits

Author SHA1 Message Date
bes-r
7dd83f0a88 v0.4.5 - releases + update-check
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m2s
Deploy server-up (prod) / deploy (push) Successful in 41s
Release / release (push) Successful in 5s
2026-06-06 22:04:04 +02:00
bes-r
7dfb6a0abb v0.4.4 - versienummer uit centraal VERSION-bestand
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 46s
2026-06-06 21:36:32 +02:00
bes-r
1eab7c07df v0.4.3 - auto-logo's legacy-apps + fix lege Docker Images
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 53s
2026-06-06 21:13:32 +02:00
12 changed files with 415 additions and 9 deletions

View file

@ -28,11 +28,12 @@ jobs:
id: version
run: |
set -euo pipefail
BASE="$(cat VERSION 2>/dev/null || echo 0.0.0)"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
CHANNEL="release"
else
VERSION="main-${GITHUB_SHA:0:8}"
VERSION="${BASE}-rc.${GITHUB_SHA:0:8}"
CHANNEL="main"
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"

View file

@ -27,11 +27,12 @@ jobs:
id: version
run: |
set -euo pipefail
BASE="$(cat VERSION 2>/dev/null || echo 0.0.0)"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
CHANNEL="release"
else
VERSION="dev-${GITHUB_SHA:0:8}"
VERSION="${BASE}-dev.${GITHUB_SHA:0:8}"
CHANNEL="dev"
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"

View file

@ -0,0 +1,112 @@
# Forgejo Actions — maak automatisch een Release bij een v*-tag.
#
# Bouwt de release-notes uit:
# 1) de bijbehorende sectie in CHANGELOG.md
# 2) een commit-overzicht sinds de vorige tag
# en publiceert die als Forgejo-Release via de API. Pre-release tags
# (bv. v0.8.4-beta1) worden als pre-release gemarkeerd.
#
# Token: gebruikt de automatische GITHUB_TOKEN van Forgejo Actions. Werkt dat
# niet (onvoldoende rechten), zet dan een secret RELEASE_TOKEN met repo-write.
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: [self-hosted, prod]
timeout-minutes: 10
steps:
- name: Checkout (volledige historie + tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release-notes opbouwen
id: notes
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
echo "TAG=${TAG}" >> "$GITHUB_ENV"
# Pre-release? (tag bevat een suffix met streepje, bv. -beta1 / -rc.1)
if [[ "$VERSION" == *-* ]]; then
echo "PRERELEASE=true" >> "$GITHUB_ENV"
else
echo "PRERELEASE=false" >> "$GITHUB_ENV"
fi
NOTES="$(mktemp)"
# 1) Sectie uit CHANGELOG.md (van "# vX..." tot de volgende "# v...")
awk -v ver="$VERSION" '
$0 ~ ("^# v?" ver "([ \t).—-]|$)") {grab=1; print; next}
grab && /^# v/ {exit}
grab {print}
' CHANGELOG.md > "$NOTES" || true
# 2) Commit-overzicht sinds de vorige tag
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")"
{
echo ""
echo "## Commits"
if [ -n "$PREV" ]; then
git log --no-merges --pretty="- %s (%h)" "${PREV}..${TAG}"
echo ""
echo "**Volledige changelog:** ${PREV}...${TAG}"
else
git log --no-merges --pretty="- %s (%h)" "${TAG}"
fi
} >> "$NOTES"
echo "NOTES_FILE=${NOTES}" >> "$GITHUB_ENV"
echo "----- release notes -----"; cat "$NOTES"
- name: Release aanmaken via Forgejo API
run: |
set -euo pipefail
TOKEN="${{ secrets.RELEASE_TOKEN }}"
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}"; fi
# Bouw JSON-payload (body veilig ge-escaped via python)
python3 - "$TAG" "$NOTES_FILE" "$PRERELEASE" > payload.json <<'PY'
import json, sys
tag, notes_file, pre = sys.argv[1], sys.argv[2], sys.argv[3]
body = open(notes_file, encoding="utf-8").read()
print(json.dumps({
"tag_name": tag,
"name": tag,
"body": body,
"draft": False,
"prerelease": pre == "true",
}))
PY
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases"
code=$(curl -sS -o resp.json -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d @payload.json "$API")
echo "HTTP ${code}"; cat resp.json || true
if [ "$code" = "201" ]; then
echo "Release ${TAG} aangemaakt."
elif [ "$code" = "409" ]; then
# Bestaat al: update de bestaande release
ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")
if [ -n "$ID" ]; then
curl -fsS -X PATCH \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d @payload.json "${API}/${ID}" >/dev/null
echo "Bestaande release ${TAG} bijgewerkt."
fi
else
echo "Release aanmaken faalde (HTTP ${code})"; exit 1
fi

View file

@ -1,3 +1,76 @@
# v0.4.5 — Releases & in-app update-check
## Nieuw in v0.4.5
### 🏷️ Automatische releases bij een tag
Push je een `v*`-tag, dan maakt de nieuwe `release.yml`-workflow automatisch een
Forgejo-**Release** aan met notes uit de bijbehorende `CHANGELOG.md`-sectie plus
een commit-overzicht sinds de vorige tag. Pre-release tags (bv. `v0.8.4-beta1`)
worden als pre-release gemarkeerd. Zo krijg je een Releases-pagina met duidelijke
changelog per versie, vergelijkbaar met GitHub Releases.
### 🔔 In-app "update beschikbaar"
Server Up vergelijkt de draaiende versie met de laatste release via de Forgejo/
GitHub Releases-API (semver-vergelijking, pre-releases optioneel). Is er een
nieuwere versie, dan verschijnt een melding in de topbar en een blok in
Instellingen → Updates met versie, release-notes en een link. Instelbaar via
`UPDATE_API_URL` (of env `SU_UPDATE_API`) en de optie "pre-releases meenemen".
### 🔖 Versiebeleid
Bump bij elke wijziging het centrale `VERSION`-bestand; tags zijn de bron voor
release-versies (`vX.Y.Z`, of `-beta`/`-rc` voor pre-releases).
---
# v0.4.4 — Nette versienummers op alle builds
## Nieuw in v0.4.4
Centraal `VERSION`-bestand (semver) is nu de bron voor het versienummer. De
deploy-workflows lezen het en bouwen:
- tag `v0.4.4``0.4.4` (productie, toont `v0.4.4`)
- push naar `main``0.4.4-rc.<sha>`
- push naar `dev``0.4.4-dev.<sha>`
Zo zie je voortaan het échte versienummer in de topbar i.p.v. alleen de
git-SHA (`vdev-1eab7c07`). Bump bij elke wijziging alleen nog `VERSION` (en voor
de zekerheid de fallback in `app.py`/`index.html` voor lokale runs).
---
# v0.4.3 — Auto-logo's legacy-apps + fix lege Docker Images
## Nieuw / fixes in v0.4.3
### 🖼️ Automatische logo's voor legacy-apps
Legacy-apps (eigen `app.json`/`stack.json`-stacks) krijgen nu automatisch een
logo via de dashboard-icons CDN, afgeleid uit de app-naam (bv. "Nextcloud" →
`nextcloud.png`). Een expliciete logo-URL in de metadata wint; bestaat het
geraden icoon niet, dan valt de UI terug op het emoji-icoon. Geldt voor zowel
de App Store als geïnstalleerde stacks.
### 🐛 Fix: Docker Images-pagina was leeg
De afbeeldingenlijst gebruikte `:key="img.id"`, maar meerdere tags kunnen
dezelfde image-ID delen → dubbele Alpine-keys waardoor de tabel niet rendert
(en de "geen images"-melding ook niet, want er waren wél images). De `x-for`
gebruikt nu de index als key.
---
# v0.4.2 — App-logo's bij stacks
## Nieuw in v0.4.2
Geïnstalleerde stacks tonen nu het app-logo (of een emoji-icoon als fallback),
zowel op het dashboard als op de Stacks-pagina. Bij installatie wordt het
logo/icoon van de bron-app opgeslagen in `.serverup.json` in de stack-map.
Bestaande installaties krijgen hun logo via een naam-match met de App Store,
dus ze hoeven niet opnieuw geïnstalleerd te worden. De store toonde al logo's
voor boilerplate-apps (via de selfhst/dashboard-icons CDN).
---
# v0.4.1 — Fix: styling werd niet toegepast
## Fix bovenop v0.4.0

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.4.5

View file

@ -13,12 +13,12 @@ from flask import Flask, Response, jsonify, render_template, request
import core as cfg
from core import audit, jobs, i18n
from core import docker, git, boilerplates
from core import docker, git, boilerplates, updater
from core.modules import Module, CORE, discover
app = Flask(__name__, static_folder="static", template_folder="templates")
VERSION = os.environ.get("SU_VERSION", "0.4.1")
VERSION = os.environ.get("SU_VERSION", "0.4.5")
MODULES: list[Module] = []
USER_MOD = APP / "modules"
@ -133,6 +133,11 @@ def api_version():
return jsonify(version=VERSION)
@app.route("/api/update")
def api_update():
return jsonify(updater.check(VERSION))
@app.route("/api/docker/images")
def api_images():
return jsonify(images=docker.images())
@ -173,6 +178,37 @@ def api_stacks():
# Bescherm tegen verkeerde LIBRARY_DIR
if lib == Path("/app").resolve():
return jsonify([])
# Logo/icon-lijst uit de store opbouwen (best effort) voor naam-fallback
icon_map = {}
try:
c = cfg.load()
for repo in c.get("APP_REPOS", []):
for st in git.scan_stacks(repo.get("id", ""), lib, subdir=repo.get("subdir", "")):
key = st.get("dir") or st.get("name")
if key and (st.get("logo_url") or st.get("icon")):
icon_map.setdefault(key, {"logo_url": st.get("logo_url", ""),
"icon": st.get("icon", "")})
except Exception:
pass
def _stack_logo(d):
# 1) Voorkeur: opgeslagen metadata bij installatie
mf = d / ".serverup.json"
if mf.exists():
try:
m = json.loads(mf.read_text("utf-8"))
if m.get("logo_url") or m.get("icon"):
return m.get("logo_url", ""), m.get("icon", "")
except Exception:
pass
# 2) Fallback: match op naam met de store (ook voor oudere installaties)
if d.name in icon_map:
h = icon_map[d.name]; return h["logo_url"], h["icon"]
for key, h in icon_map.items():
if d.name.startswith(key + "-"):
return h["logo_url"], h["icon"]
return "", ""
out = []
for d in sorted(lib.iterdir()):
if not d.is_dir() or d.name.startswith("."):
@ -180,10 +216,12 @@ def api_stacks():
if not docker.has_compose(d):
continue
ct = docker.compose_ps(d, name=d.name)
logo_url, icon = _stack_logo(d)
out.append({
"name": d.name, "path": str(d), "has_compose": True,
"running": any(c["running"] for c in ct),
"containers": ct,
"logo_url": logo_url, "icon": icon,
})
return jsonify(out)
@ -637,6 +675,22 @@ def api_store_install():
except Exception:
pass
# Bewaar logo/icon-metadata zodat de stack-lijst het kan tonen
try:
meta = boilerplates.metadata(src) if is_bp else git._meta(src)
logo = meta.get("logo_url", "")
if not logo and not is_bp:
logo = git.guess_logo_url(meta.get("name") or stack)
(dest / ".serverup.json").write_text(json.dumps({
"source": stack,
"repo_id": rid,
"name": meta.get("name", stack),
"logo_url": logo,
"icon": meta.get("icon", ""),
}, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception:
pass
# Start
jobs.log(qq, "section", "Starten")
rc = docker.compose_up(dest, log_fn=lambda m: jobs.log(qq, "dim", m), name=inst)

View file

@ -23,6 +23,11 @@ DEFAULTS = {
"LANGUAGE": "nl",
"THEME": "dark",
"WIZARD_DONE": False,
# Update-check: Forgejo/GitHub Releases-API die de laatste release teruggeeft.
# Leeg = uitgeschakeld. Voorbeeld (Forgejo):
# http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases
"UPDATE_API_URL": os.environ.get("SU_UPDATE_API", ""),
"UPDATE_INCLUDE_PRERELEASE": False,
}
_path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json"))

View file

@ -1,9 +1,18 @@
"""Git operations — clone, pull, scan for stacks and modules."""
import json, os, subprocess, shutil
import json, os, re, subprocess, shutil
from pathlib import Path
from core.docker import COMPOSE_NAMES
from core import boilerplates as bp
def guess_logo_url(name: str) -> str:
"""Leid een dashboard-icons logo-URL af uit een app-naam.
Bestaat het icoon niet, dan faalt de <img> en valt de UI terug op de emoji."""
slug = re.sub(r"[^a-z0-9]+", "-", (name or "").lower()).strip("-")
if not slug:
return ""
return f"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/{slug}.png"
CACHE = Path(os.environ.get("SU_GIT_CACHE", "/data/git"))
# Fix git "dubious ownership" — container draait als root, cache kan door andere uid zijn
@ -129,6 +138,14 @@ def _scan_compose_dirs(base: Path, lib: Path) -> list[dict]:
else:
meta = _meta(d)
meta.setdefault("format", "compose")
# Auto-logo voor legacy-apps: gebruik expliciete logo_url, anders raden
# op basis van de naam (dashboard-icons). Emoji blijft fallback in de UI.
if not meta.get("logo_url"):
ic = meta.get("icon", "")
if isinstance(ic, str) and (ic.startswith("http://") or ic.startswith("https://")):
meta["logo_url"] = ic
else:
meta["logo_url"] = guess_logo_url(meta.get("name") or d.name)
installed = []
if lib.exists():

69
server-up/core/updater.py Normal file
View file

@ -0,0 +1,69 @@
"""Update-check tegen een Forgejo/GitHub Releases-API (stdlib only)."""
from __future__ import annotations
import json
import re
import urllib.request
import core as cfg
_SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+.](.+))?$")
def _pre_key(pre: str):
parts = [p for p in re.split(r"[.+-]", pre) if p] if pre else []
return tuple((0, int(p)) if p.isdigit() else (1, p) for p in parts)
def _parse(v: str):
"""Naar vergelijkbare tuple. Release sorteert hoger dan pre-release."""
if not v:
return None
m = _SEMVER.match(v.strip())
if not m:
return None
pre = m.group(4) or ""
return (int(m.group(1)), int(m.group(2)), int(m.group(3)),
1 if not pre else 0, _pre_key(pre))
def check(current: str) -> dict:
c = cfg.load()
url = (c.get("UPDATE_API_URL") or "").strip()
include_pre = bool(c.get("UPDATE_INCLUDE_PRERELEASE"))
res = {"enabled": bool(url), "current": current, "latest": None,
"update_available": False, "url": "", "notes": "",
"prerelease": False, "error": ""}
if not url:
return res
try:
req = urllib.request.Request(
url, headers={"Accept": "application/json", "User-Agent": "server-up"})
with urllib.request.urlopen(req, timeout=8) as r:
data = json.loads(r.read().decode("utf-8"))
except Exception as e:
res["error"] = f"{type(e).__name__}: {e}"
return res
rels = data if isinstance(data, list) else [data]
best, best_t = None, None
for rel in rels:
if not isinstance(rel, dict) or rel.get("draft"):
continue
if rel.get("prerelease") and not include_pre:
continue
t = _parse(rel.get("tag_name") or rel.get("name") or "")
if not t:
continue
if best_t is None or t > best_t:
best, best_t = rel, t
if not best:
return res
res["latest"] = (best.get("tag_name") or best.get("name") or "").lstrip("v")
res["url"] = best.get("html_url") or best.get("url") or ""
res["notes"] = best.get("body") or ""
res["prerelease"] = bool(best.get("prerelease"))
cur_t = _parse(current)
if cur_t and best_t and best_t > cur_t:
res["update_available"] = True
return res

View file

@ -126,6 +126,11 @@ tailwind.config = {
<span class="w-px h-4 bg-bd dark:bg-bd-dark mx-1"></span>
<span class="mdi mdi-layers-outline text-bl"></span><span x-text="docker.images ?? ''"></span>
</div>
<a x-show="update.update_available" x-cloak :href="update.url" target="_blank" rel="noopener"
:title="t('update_available')"
class="flex items-center gap-1.5 px-3 h-10 rounded-xl bg-ac/10 text-ac dark:text-ac-dark text-xs font-bold hover:bg-ac/20 transition">
<span class="mdi mdi-arrow-up-bold-circle text-base"></span><span x-text="'v'+update.latest"></span>
</a>
<button @click="toggleTheme()" :title="t('theme')" class="btn-icon">
<span class="mdi text-xl" :class="theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"></span>
</button>
@ -164,6 +169,8 @@ tailwind.config = {
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" x-show="stacks.length > 0">
<template x-for="s in stacks.slice(0, 6)" :key="s.name">
<button @click="goto('stacks')" class="card card-hover p-4 flex items-center gap-3 text-left">
<template x-if="s.logo_url"><img :src="s.logo_url" class="w-8 h-8 rounded-lg object-contain bg-s2 dark:bg-s2-dark p-0.5 shrink-0" loading="lazy" onerror="this.style.display='none'"></template>
<template x-if="!s.logo_url"><span class="w-8 h-8 rounded-lg bg-s2 dark:bg-s2-dark grid place-items-center text-base shrink-0" x-text="s.icon || '📦'"></span></template>
<span class="w-2.5 h-2.5 rounded-full shrink-0" :class="s.running ? 'bg-ok shadow-[0_0_8px_#22c55e]' : 'bg-dm dark:bg-dm-dark'"></span>
<span class="font-semibold text-wh dark:text-wh-dark text-sm flex-1 truncate" x-text="s.name"></span>
<span class="badge" :class="s.running ? 'badge-ok' : 'badge-dim'" x-text="s.running ? t('running') : t('stopped')"></span>
@ -186,6 +193,8 @@ tailwind.config = {
<template x-for="s in filteredStacks" :key="s.name">
<div class="card card-hover flex flex-col">
<div class="px-4 py-3.5 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
<template x-if="s.logo_url"><img :src="s.logo_url" class="w-8 h-8 rounded-lg object-contain bg-s2 dark:bg-s2-dark p-0.5 shrink-0" loading="lazy" onerror="this.style.display='none'"></template>
<template x-if="!s.logo_url"><span class="w-8 h-8 rounded-lg bg-s2 dark:bg-s2-dark grid place-items-center text-base shrink-0" x-text="s.icon || '📦'"></span></template>
<span class="w-2.5 h-2.5 rounded-full shrink-0" :class="!s.has_compose ? 'bg-wr' : s.running ? 'bg-ok shadow-[0_0_8px_#22c55e]' : 'bg-dm dark:bg-dm-dark'"></span>
<span class="font-semibold text-wh dark:text-wh-dark text-sm flex-1 truncate" x-text="s.name"></span>
<span class="badge" :class="s.running ? 'badge-ok' : !s.has_compose ? 'badge-warn' : 'badge-dim'" x-text="!s.has_compose ? '—' : s.running ? t('running') : t('stopped')"></span>
@ -285,7 +294,7 @@ tailwind.config = {
<th class="px-4 py-3 font-bold">Size</th><th class="px-4 py-3 font-bold">Age</th><th class="px-4 py-3"></th>
</tr></thead>
<tbody>
<template x-for="img in images" :key="img.id">
<template x-for="(img, i) in images" :key="i">
<tr class="border-t border-bd dark:border-bd-dark hover:bg-s2/60 dark:hover:bg-s2-dark/60 transition">
<td class="px-4 py-2.5 font-mono text-tx dark:text-tx-dark" x-text="img.repo"></td>
<td class="px-4 py-2.5 font-mono text-ac dark:text-ac-dark" x-text="img.tag"></td>
@ -356,6 +365,44 @@ tailwind.config = {
</div>
</div>
<!-- Updates -->
<div class="card">
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
<span class="mdi mdi-update text-ac dark:text-ac-dark text-lg"></span>
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('updates')">Updates</span>
<template x-if="update.update_available"><span class="badge badge-ok" x-text="t('update_available')">update beschikbaar</span></template>
<template x-if="update.enabled && !update.update_available && !update.error"><span class="badge badge-dim" x-text="t('up_to_date')">up-to-date</span></template>
<button class="btn-sm ml-auto" @click="loadUpdate()"><span class="mdi mdi-refresh"></span> <span x-text="t('check_now')">Controleren</span></button>
</div>
<div class="p-5 space-y-4">
<div class="flex flex-wrap gap-x-8 gap-y-2 text-sm">
<div><div class="form-label" x-text="t('current_version')">Huidige versie</div><div class="font-mono font-semibold text-wh dark:text-wh-dark" x-text="'v'+(update.current||version)"></div></div>
<div x-show="update.latest"><div class="form-label" x-text="t('latest_version')">Laatste release</div><div class="font-mono font-semibold" :class="update.update_available ? 'text-ac dark:text-ac-dark' : 'text-wh dark:text-wh-dark'" x-text="'v'+update.latest"></div></div>
</div>
<template x-if="update.update_available && update.notes">
<details class="rounded-xl border border-bd dark:border-bd-dark overflow-hidden">
<summary class="px-3.5 py-2.5 cursor-pointer font-bold text-xs text-wh dark:text-wh-dark bg-s2/60 dark:bg-s2-dark/60 flex items-center gap-2"><span class="mdi mdi-text-box-outline"></span> <span x-text="t('release_notes')">Release notes</span></summary>
<pre class="p-3.5 text-xs whitespace-pre-wrap break-words text-t2 dark:text-t2-dark max-h-64 overflow-auto" x-text="update.notes"></pre>
</details>
</template>
<template x-if="update.update_available">
<a :href="update.url" target="_blank" rel="noopener" class="btn btn-primary w-max"><span class="mdi mdi-open-in-new"></span> <span x-text="t('view_release')">Bekijk release</span></a>
</template>
<p x-show="update.error" class="text-xs text-er" x-text="update.error"></p>
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
<div>
<label class="form-label" x-text="t('update_api_url')">Releases-API URL</label>
<input class="form-input font-mono" x-model="settings.UPDATE_API_URL" @change="autoSave(); setTimeout(()=>loadUpdate(), 600)" placeholder="http://git.example.com/api/v1/repos/bes-r/server-up/releases">
<p class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="t('update_api_hint')">Forgejo/GitHub Releases-API. Leeg = uit.</p>
</div>
<label class="inline-flex items-center gap-2.5 cursor-pointer">
<input type="checkbox" :checked="!!settings.UPDATE_INCLUDE_PRERELEASE" @change="settings.UPDATE_INCLUDE_PRERELEASE=$event.target.checked; autoSave(); setTimeout(()=>loadUpdate(), 600)" class="w-5 h-5 rounded border-bd dark:border-bd-dark text-ac">
<span class="text-sm text-tx dark:text-tx-dark" x-text="t('include_prerelease')">Pre-releases (beta) meenemen</span>
</label>
</div>
</div>
</div>
<!-- Taalmodule (add-on) -->
<div class="card">
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
@ -724,7 +771,7 @@ tailwind.config = {
<script>
function app() {
return {
version: '0.4.1',
version: '0.4.5',
page: location.hash.slice(1) || 'dashboard',
isMobile: window.innerWidth < 768,
sidebarOpen: window.innerWidth >= 768 && localStorage.getItem('sidebar') !== 'closed',
@ -746,6 +793,7 @@ function app() {
settings: {},
languages: [],
modules: [],
update: { enabled:false, update_available:false, current:'', latest:'', url:'', notes:'', prerelease:false },
T: {},
job: { id: null, lines: [], status: 'idle', timer: null },
@ -790,6 +838,7 @@ function app() {
await this.loadDocker();
await this.loadModules();
await this.loadDashboard();
this.loadUpdate();
setInterval(() => { if (this.page==='dashboard') this.loadDocker(); }, 10000);
this.$watch('page', async (p) => {
if (p === 'stacks') this.loadStacks();
@ -1076,6 +1125,8 @@ function app() {
async loadModules() { const d = await this.rpc('/api/modules'); this.modules = Array.isArray(d) ? d : []; },
async loadUpdate() { const d = await this.rpc('/api/update'); if (d && typeof d === 'object') this.update = d; },
async checkWizard() {
const d = await this.rpc('/api/wizard');
if (!d.done) {

View file

@ -104,5 +104,16 @@
"lang_name": "Name",
"lang_flag": "Flag",
"search_keys": "Search key or text…",
"delete_language_confirm": "Are you sure you want to delete this language?"
"delete_language_confirm": "Are you sure you want to delete this language?",
"updates": "Updates",
"update_available": "Update available",
"up_to_date": "Up to date",
"check_now": "Check now",
"current_version": "Current version",
"latest_version": "Latest release",
"release_notes": "Release notes",
"view_release": "View release",
"update_api_url": "Releases API URL",
"update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.",
"include_prerelease": "Include pre-releases (beta)"
}

View file

@ -104,5 +104,16 @@
"lang_name": "Naam",
"lang_flag": "Vlag",
"search_keys": "Zoek vertaalsleutel of tekst…",
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?"
"delete_language_confirm": "Weet je zeker dat je deze taal wilt verwijderen?",
"updates": "Updates",
"update_available": "Update beschikbaar",
"up_to_date": "Up-to-date",
"check_now": "Controleren",
"current_version": "Huidige versie",
"latest_version": "Laatste release",
"release_notes": "Release notes",
"view_release": "Bekijk release",
"update_api_url": "Releases-API URL",
"update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.",
"include_prerelease": "Pre-releases (beta) meenemen"
}