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

This commit is contained in:
bes-r 2026-06-06 21:13:32 +02:00
parent 9b388af0cc
commit 1eab7c07df
4 changed files with 106 additions and 4 deletions

View file

@ -1,3 +1,35 @@
# 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

View file

@ -18,7 +18,7 @@ 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.3")
MODULES: list[Module] = []
USER_MOD = APP / "modules"
@ -173,6 +173,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 +211,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 +670,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

@ -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():

View file

@ -164,6 +164,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 +188,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 +289,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>
@ -724,7 +728,7 @@ tailwind.config = {
<script>
function app() {
return {
version: '0.4.1',
version: '0.4.3',
page: location.hash.slice(1) || 'dashboard',
isMobile: window.innerWidth < 768,
sidebarOpen: window.innerWidth >= 768 && localStorage.getItem('sidebar') !== 'closed',