Compare commits
3 commits
9b388af0cc
...
7dd83f0a88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd83f0a88 | ||
|
|
7dfb6a0abb | ||
|
|
1eab7c07df |
12 changed files with 415 additions and 9 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
112
.forgejo/workflows/release.yml
Normal file
112
.forgejo/workflows/release.yml
Normal 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
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
|
|
@ -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
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.4.5
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
69
server-up/core/updater.py
Normal 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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue