v0.8.50-beta - Leesbare namen, zichtbaar wat al draait, en bruikbaar op een telefoon
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 14m6s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 14m6s
- Het auditlog toonde ruwe id's als 'store', 'reconfigure' en 'credentials_view'. Die hebben nu namen, ook in de filterlijsten, en de kolomkoppen zijn vertaald in plaats van hardgecodeerd Nederlands. Onbekende waarden vallen terug op de ruwe tekst zodat een module niets kapotmaakt - Nieuwe test die de broncode afloopt en elke audit.log() zonder vertaling aanwijst - Een modulepagina heette in de titelbalk letterlijk 'mod:mijnmodule' - De store herkende een installatie aan de mapnaam: een stack telde alleen mee als hij 'sonarr' of 'sonarr-...' heette. Wie zijn stack 'media-tv' noemde kreeg te zien dat Sonarr niet geinstalleerd was. Nu via de source uit .serverup.json, met een duidelijk Geinstalleerd-label per installatie waar je op kunt klikken om ernaartoe te gaan - Twaalf icoonknoppen per stack werden op een telefoon drie rijen plaatjes zonder naam. Daar blijven starten, herstarten en logs staan, plus een menu met de rest, mét namen. Op een breed scherm verandert er niets - De rij van twintig categorieknoppen boven de stacks is hetzelfde compacte menu geworden als in de app store Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
parent
072fdff3a2
commit
1db3d3e8a0
9 changed files with 499 additions and 46 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -1,3 +1,31 @@
|
|||
# v0.8.50-beta — Leesbare namen, zichtbaar wat al draait, en bruikbaar op een telefoon
|
||||
|
||||
**Het auditlog toonde ruwe id's.** In de kolommen stond `store`, `reconfigure`,
|
||||
`credentials_view` — de sleutels waarmee de code logt, niet iets wat je leest. Die
|
||||
hebben nu Nederlandse namen ("App Store", "Opnieuw ingesteld", "Inloggegevens
|
||||
bekeken"), ook in de filterlijsten erboven, en de kolomkoppen zijn niet langer
|
||||
hardgecodeerd Nederlands maar vertaald. Een categorie die we niet kennen — van een
|
||||
module bijvoorbeeld — valt terug op de ruwe waarde in plaats van leeg te blijven.
|
||||
Er draait een test die de broncode afloopt en elke `audit.log(...)` zonder
|
||||
vertaling aanwijst. Een modulepagina heette in de titelbalk letterlijk
|
||||
`mod:mijnmodule`; die toont nu de naam van de module.
|
||||
|
||||
**De store zag niet dat een app al draaide.** Er zat een klein groen vinkje naast
|
||||
de installeerknop, en dat werkte alleen als je de stack precies zo had genoemd als
|
||||
het sjabloon: `sonarr` of `sonarr-iets`. Noemde je hem `media-tv`, dan bleef de
|
||||
store volhouden dat Sonarr nog niet geïnstalleerd was. Nu leest hij `.serverup.json`
|
||||
uit — dezelfde `source` die het herconfigureerscherm al gebruikte — en toont een
|
||||
duidelijk **Geïnstalleerd**-label met de naam van elke draaiende installatie,
|
||||
waar je op kunt klikken om er meteen naartoe te gaan. De knop eronder zegt dan
|
||||
"Nog een keer" in plaats van "Installeren".
|
||||
|
||||
**Op een telefoon was de stackpagina niet te doen.** Elke stack had twaalf
|
||||
icoonknoppen zonder tekst naast elkaar, wat op een smal scherm drie rijen
|
||||
plaatjes werd. Daar staan nu starten, herstarten en logs, plus een menu met de
|
||||
rest — mét namen erbij. Op een breed scherm verandert er niets. De rij van twintig
|
||||
categorieknoppen boven de stacks is hetzelfde menu geworden als in de app store,
|
||||
en "Updates controleren" verliest op smalle schermen zijn label.
|
||||
|
||||
# v0.8.40-beta — De filter in de app store is een menu geworden
|
||||
|
||||
**Twee rijen knoppen vulden een half scherm.** Boven de app store stonden twintig
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.8.40-beta
|
||||
0.8.50-beta
|
||||
|
|
|
|||
|
|
@ -140,11 +140,39 @@ def scan_dir(base: Path, lib: Path) -> list[dict]:
|
|||
return _scan_compose_dirs(base, lib)
|
||||
|
||||
|
||||
def _installaties_per_sjabloon(lib: Path) -> dict[str, list[str]]:
|
||||
"""Welke stacks zijn uit welk sjabloon geïnstalleerd?
|
||||
|
||||
Dit stond eerder op de mapnaam: een stack telde als installatie van
|
||||
`sonarr` zodra hij `sonarr` of `sonarr-…` heette. Wie zijn stack `media-tv`
|
||||
noemde, kreeg in de store te zien dat Sonarr nog niet geïnstalleerd was.
|
||||
`.serverup.json` weet wél uit welk sjabloon hij komt — dezelfde `source` die
|
||||
het herconfigureerscherm en `depends_on` gebruiken.
|
||||
"""
|
||||
uit: dict[str, list[str]] = {}
|
||||
if not lib.exists():
|
||||
return uit
|
||||
for d in sorted(lib.iterdir()):
|
||||
if not d.is_dir() or d.name.startswith("."):
|
||||
continue
|
||||
mf = d / ".serverup.json"
|
||||
if not mf.exists():
|
||||
continue
|
||||
try:
|
||||
bron = (json.loads(mf.read_text("utf-8")) or {}).get("source") or ""
|
||||
except Exception:
|
||||
continue
|
||||
if bron:
|
||||
uit.setdefault(bron, []).append(d.name)
|
||||
return uit
|
||||
|
||||
|
||||
def _scan_compose_dirs(base: Path, lib: Path) -> list[dict]:
|
||||
"""Scan één directory voor compose stacks. Detecteert zowel het klassieke
|
||||
Server Up formaat (compose.yml + stack.json) als het ChristianLempa
|
||||
Boilerplates formaat (template.json + files/)."""
|
||||
stacks = []
|
||||
geinstalleerd = _installaties_per_sjabloon(lib)
|
||||
for d in sorted(base.iterdir()):
|
||||
if not d.is_dir() or d.name.startswith((".", "_")):
|
||||
continue
|
||||
|
|
@ -171,11 +199,13 @@ def _scan_compose_dirs(base: Path, lib: Path) -> list[dict]:
|
|||
else:
|
||||
meta["logo_url"] = guess_logo_url(meta.get("name") or d.name)
|
||||
|
||||
installed = []
|
||||
installed = set(geinstalleerd.get(d.name, ()))
|
||||
# Vangnet voor installaties van vóór de metadata, en voor mappen die
|
||||
# iemand met de hand heeft neergezet.
|
||||
if lib.exists():
|
||||
for ld in lib.iterdir():
|
||||
if ld.is_dir() and (ld.name == d.name or ld.name.startswith(d.name + "-")):
|
||||
installed.append(ld.name)
|
||||
installed.add(ld.name)
|
||||
entry = {
|
||||
"dir": d.name,
|
||||
"name": meta.pop("name", d.name),
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ tailwind.config = {
|
|||
<button @click="toggleSidebar()" :title="sidebarOpen ? t('hide_menu') : t('show_menu')" class="btn-icon">
|
||||
<span class="mdi text-xl" :class="sidebarOpen ? 'mdi-menu-open' : 'mdi-menu'"></span>
|
||||
</button>
|
||||
<h1 class="text-lg font-bold text-wh dark:text-wh-dark tracking-tight truncate" x-text="t(page === 'docker' ? 'images' : page)">Dashboard</h1>
|
||||
<h1 class="text-lg font-bold text-wh dark:text-wh-dark tracking-tight truncate" x-text="paginaTitel">Dashboard</h1>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="hidden sm:flex items-center gap-1.5 px-3 h-10 rounded-xl bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark text-xs font-mono text-t2 dark:text-t2-dark">
|
||||
|
|
@ -250,29 +250,52 @@ tailwind.config = {
|
|||
<span class="mdi mdi-magnify absolute left-3.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark text-lg"></span>
|
||||
<input x-model="stackQuery" :placeholder="t('search')+'…'" class="form-input !pl-10">
|
||||
</div>
|
||||
<button @click="loadStacks()" class="btn"><span class="mdi mdi-refresh"></span> <span x-text="t('refresh')">Vernieuwen</span></button>
|
||||
<!-- Zelfde menu als in de store; de rij van twintig categorieknoppen
|
||||
die hier stond, paste op een telefoon niet. -->
|
||||
<div class="relative" x-show="stackCategories.length"
|
||||
@keydown.escape.window="stackFilterOpen = false">
|
||||
<button type="button" class="btn !px-3" @click="stackFilterOpen = !stackFilterOpen"
|
||||
:class="stackCat ? '!border-ac !text-ac dark:!text-ac-dark !bg-ac/5' : ''">
|
||||
<span class="mdi mdi-tune-variant"></span>
|
||||
<span x-text="stackCat ? cat(stackCat).name : t('filter')">Filter</span>
|
||||
<span class="mdi text-xs transition-transform"
|
||||
:class="stackFilterOpen ? 'mdi-chevron-down rotate-180' : 'mdi-chevron-down'"></span>
|
||||
</button>
|
||||
<div x-show="stackFilterOpen" x-cloak @click.outside="stackFilterOpen = false"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||
class="card shadow-pop absolute left-0 z-40 mt-2 w-[min(92vw,24rem)] origin-top p-3">
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<button type="button" @click="stackCat = ''"
|
||||
class="flex items-center gap-2 px-2.5 py-1.5 rounded-xl border text-[11px] font-semibold transition col-span-2"
|
||||
:class="!stackCat ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'">
|
||||
<span class="mdi mdi-select-all"></span>
|
||||
<span class="flex-1 text-left" x-text="t('all')">Alle</span>
|
||||
<span class="opacity-60 tabular-nums" x-text="stacks.length"></span>
|
||||
</button>
|
||||
<template x-for="c in stackCategories" :key="c.id">
|
||||
<button type="button"
|
||||
class="flex items-center gap-2 px-2.5 py-1.5 rounded-xl border text-[11px] font-semibold transition text-left"
|
||||
:style="stackCat === c.id ? catStyle(c.id) : ''"
|
||||
:class="stackCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'"
|
||||
@click="stackCat = (stackCat === c.id ? '' : c.id); stackFilterOpen = false">
|
||||
<span class="mdi shrink-0" :class="c.icon"
|
||||
:style="stackCat === c.id ? '' : 'color:' + c.color"></span>
|
||||
<span class="truncate flex-1" x-text="c.name"></span>
|
||||
<span class="opacity-60 tabular-nums" x-text="c.count"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="loadStacks()" class="btn-icon ml-auto" :title="t('refresh')"><span class="mdi mdi-refresh"></span></button>
|
||||
<button @click="checkStackUpdates()" class="btn" :disabled="checkingUpd">
|
||||
<span class="mdi" :class="checkingUpd ? 'mdi-loading mdi-spin' : 'mdi-package-up'"></span>
|
||||
<span x-text="t('check_updates')">Updates controleren</span>
|
||||
<span class="hidden sm:inline" x-text="t('check_updates')">Updates controleren</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Categorieën van wat je hebt draaien; klikken filtert. -->
|
||||
<div class="flex flex-wrap gap-1.5 mb-5" x-show="stackCategories.length">
|
||||
<button class="text-[11px] font-semibold px-2.5 py-1 rounded-lg border transition"
|
||||
:class="!stackCat ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
||||
@click="stackCat = ''" x-text="t('all')">Alle</button>
|
||||
<template x-for="c in stackCategories" :key="c.id">
|
||||
<button class="text-[11px] font-semibold px-2.5 py-1 rounded-lg border transition inline-flex items-center gap-1.5"
|
||||
:style="stackCat === c.id ? catStyle(c.id) : ''"
|
||||
:class="stackCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
||||
@click="stackCat = (stackCat === c.id ? '' : c.id)">
|
||||
<span class="mdi" :class="c.icon" :style="stackCat === c.id ? '' : 'color:' + c.color"></span>
|
||||
<span x-text="c.name"></span>
|
||||
<span class="opacity-60" x-text="c.count"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" x-show="filteredStacks.length > 0">
|
||||
<template x-for="s in filteredStacks" :key="s.name">
|
||||
<div class="card card-hover flex flex-col border-l-4"
|
||||
|
|
@ -352,27 +375,55 @@ tailwind.config = {
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Twaalf icoonknoppen naast elkaar werkt op een breed scherm,
|
||||
maar vult op een telefoon drie rijen met plaatjes zonder
|
||||
naam. Daar blijven starten, logs en een menu over. -->
|
||||
<div class="flex flex-wrap gap-1.5 mt-auto" x-show="s.has_compose">
|
||||
<button class="btn-sm flex-1 min-w-[84px]" :class="s.running ? 'btn-danger' : 'btn-primary'" @click="stackAction(s.name, s.running ? 'stop' : 'start')">
|
||||
<span class="mdi" :class="s.running ? 'mdi-stop' : 'mdi-play'"></span>
|
||||
<span x-text="s.running ? t('stop') : t('start')"></span>
|
||||
</button>
|
||||
<button class="btn-sm" :title="t('restart')" @click="stackAction(s.name, 'restart')"><span class="mdi mdi-restart"></span></button>
|
||||
<button class="btn-sm" :title="t('update')" @click="stackAction(s.name, 'update')"><span class="mdi mdi-update"></span></button>
|
||||
<button class="btn-sm" title=".env" @click="openEditor(s.name, 'env')"><span class="mdi mdi-file-cog-outline"></span></button>
|
||||
<button class="btn-sm" title="compose" @click="openEditor(s.name, 'compose')"><span class="mdi mdi-file-code-outline"></span></button>
|
||||
<button class="btn-sm" :title="t('logs')" @click="openLogs(s.name)"><span class="mdi mdi-text-box-outline"></span></button>
|
||||
<button class="btn-sm" :title="t('backup')" @click="stackAction(s.name, 'backup')"><span class="mdi mdi-archive-arrow-down-outline"></span></button>
|
||||
<button class="btn-sm" :title="t('credentials')" @click="openCredentials(s.name)"><span class="mdi mdi-account-key-outline"></span></button>
|
||||
<button class="btn-sm" :title="t('backups')" @click="openBackups(s.name)"><span class="mdi mdi-history"></span></button>
|
||||
<button class="btn-sm" :title="t('stack_settings')" @click="openReconfigure(s.name)"
|
||||
|
||||
<!-- Vanaf sm alles uitgeklapt -->
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="t('update')" @click="stackAction(s.name, 'update')"><span class="mdi mdi-update"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" title=".env" @click="openEditor(s.name, 'env')"><span class="mdi mdi-file-cog-outline"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" title="compose" @click="openEditor(s.name, 'compose')"><span class="mdi mdi-file-code-outline"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="t('backup')" @click="stackAction(s.name, 'backup')"><span class="mdi mdi-archive-arrow-down-outline"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="t('credentials')" @click="openCredentials(s.name)"><span class="mdi mdi-account-key-outline"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="t('backups')" @click="openBackups(s.name)"><span class="mdi mdi-history"></span></button>
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="t('stack_settings')" @click="openReconfigure(s.name)"
|
||||
:disabled="!s.editable"><span class="mdi mdi-tune"></span></button>
|
||||
<button class="btn-sm" :title="s.connected ? t('connected_to_apps') : t('connect_to_apps')"
|
||||
<button class="btn-sm hidden sm:inline-flex" :title="s.connected ? t('connected_to_apps') : t('connect_to_apps')"
|
||||
:class="s.connected ? '!text-ok !border-ok/30' : ''"
|
||||
@click="connectStack(s.name)" :disabled="s.connected">
|
||||
<span class="mdi" :class="s.connected ? 'mdi-lan-connect' : 'mdi-lan-pending'"></span>
|
||||
</button>
|
||||
<button class="btn-sm btn-danger ml-auto" :title="t('remove')" @click="confirmRemove(s.name)"><span class="mdi mdi-delete-outline"></span></button>
|
||||
<button class="btn-sm btn-danger hidden sm:inline-flex sm:ml-auto" :title="t('remove')" @click="confirmRemove(s.name)"><span class="mdi mdi-delete-outline"></span></button>
|
||||
|
||||
<!-- Op de telefoon: de rest onder één knop, mét namen -->
|
||||
<div class="relative sm:hidden ml-auto" @keydown.escape.window="stackMenu = ''">
|
||||
<button class="btn-sm" @click="stackMenu = (stackMenu === s.name ? '' : s.name)"
|
||||
:title="t('more')">
|
||||
<span class="mdi mdi-dots-horizontal"></span>
|
||||
</button>
|
||||
<div x-show="stackMenu === s.name" x-cloak @click.outside="stackMenu = ''"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
class="card shadow-pop absolute right-0 bottom-full mb-2 z-30 w-52 p-1.5 origin-bottom-right">
|
||||
<template x-for="a in stackMenuItems(s)" :key="a.id">
|
||||
<button class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-xs text-left transition"
|
||||
:class="a.gevaar ? 'text-er hover:bg-er/10' : 'text-tx dark:text-tx-dark hover:bg-s2 dark:hover:bg-s2-dark'"
|
||||
:disabled="a.uit" :style="a.uit ? 'opacity:.4' : ''"
|
||||
@click="stackMenu = ''; a.doe()">
|
||||
<span class="mdi text-base w-5 text-center" :class="a.icon"></span>
|
||||
<span x-text="a.label"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -573,6 +624,24 @@ tailwind.config = {
|
|||
</div>
|
||||
<span x-show="stack.format === 'boilerplate'" class="badge badge-info shrink-0" title="ChristianLempa Boilerplate">BP</span>
|
||||
</div>
|
||||
<!-- Draait deze app al? Dat stond eerder als een klein
|
||||
vinkje naast de knop, waar niemand het zag. -->
|
||||
<template x-if="(stack.instances || []).length">
|
||||
<div class="flex flex-wrap items-center gap-1.5 mb-3">
|
||||
<span class="badge badge-ok inline-flex items-center gap-1">
|
||||
<span class="mdi mdi-check-circle text-[11px]"></span>
|
||||
<span x-text="t('installed')">Geïnstalleerd</span>
|
||||
</span>
|
||||
<template x-for="inst in stack.instances" :key="inst">
|
||||
<button type="button" @click.stop="gaNaarStack(inst)"
|
||||
class="text-[10px] font-mono px-2 py-0.5 rounded-md border border-ok/30 bg-ok/5 text-ok hover:bg-ok/10 transition inline-flex items-center gap-1"
|
||||
:title="t('open_stack')">
|
||||
<span x-text="inst"></span>
|
||||
<span class="mdi mdi-arrow-right text-[11px]"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-1 mb-3" x-show="(stack.categories || []).length || (stack.tags || []).length">
|
||||
<template x-for="c in (stack.categories || [])" :key="c">
|
||||
<button type="button" @click.stop="storeCat = c"
|
||||
|
|
@ -587,12 +656,11 @@ tailwind.config = {
|
|||
</template>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-auto items-center">
|
||||
<button @click="openInstall(stack, repo.id)" class="btn-sm btn-primary flex-1">
|
||||
<span class="mdi mdi-download-outline"></span> <span x-text="t('install')">Installeren</span>
|
||||
<button @click="openInstall(stack, repo.id)" class="btn-sm flex-1"
|
||||
:class="(stack.instances || []).length ? '' : 'btn-primary'">
|
||||
<span class="mdi mdi-download-outline"></span>
|
||||
<span x-text="(stack.instances || []).length ? t('install_again') : t('install')">Installeren</span>
|
||||
</button>
|
||||
<template x-if="stack.instances && stack.instances.length">
|
||||
<span class="text-[11px] font-semibold text-ok px-2 grid place-items-center" :title="stack.instances.join(', ')" x-text="'✓ '+stack.instances.length"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -642,11 +710,11 @@ tailwind.config = {
|
|||
</div>
|
||||
<select class="form-input !w-auto !h-9 !text-xs" x-model="auditFilter.src" @change="loadAudit()">
|
||||
<option value="" x-text="t('all')">Alle</option>
|
||||
<template x-for="v in (auditFacets.src || [])" :key="v"><option :value="v" x-text="v"></option></template>
|
||||
<template x-for="v in (auditFacets.src || [])" :key="v"><option :value="v" x-text="auditNaam('cat', v)"></option></template>
|
||||
</select>
|
||||
<select class="form-input !w-auto !h-9 !text-xs" x-model="auditFilter.action" @change="loadAudit()">
|
||||
<option value="" x-text="t('all')">Alle</option>
|
||||
<template x-for="v in (auditFacets.action || [])" :key="v"><option :value="v" x-text="v"></option></template>
|
||||
<template x-for="v in (auditFacets.action || [])" :key="v"><option :value="v" x-text="auditNaam('act', v)"></option></template>
|
||||
</select>
|
||||
<select class="form-input !w-auto !h-9 !text-xs" x-model="auditFilter.status" @change="loadAudit()">
|
||||
<option value="" x-text="t('all')">Alle</option>
|
||||
|
|
@ -670,16 +738,19 @@ tailwind.config = {
|
|||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="bg-s2 dark:bg-s2-dark text-[10px] uppercase tracking-wider text-dm dark:text-dm-dark text-left">
|
||||
<th class="px-4 py-3 font-bold">Tijd</th><th class="px-4 py-3 font-bold">Categorie</th>
|
||||
<th class="px-4 py-3 font-bold">Actie</th><th class="px-4 py-3 font-bold">Status</th><th class="px-4 py-3 font-bold">Ref</th>
|
||||
<th class="px-4 py-3 font-bold" x-text="t('col_time')">Tijd</th>
|
||||
<th class="px-4 py-3 font-bold" x-text="t('col_category')">Categorie</th>
|
||||
<th class="px-4 py-3 font-bold" x-text="t('col_action')">Actie</th>
|
||||
<th class="px-4 py-3 font-bold" x-text="t('col_status')">Status</th>
|
||||
<th class="px-4 py-3 font-bold" x-text="t('col_ref')">Betreft</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<template x-for="(e, i) in audit" :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 font-mono text-t2 dark:text-t2-dark whitespace-nowrap" x-text="(e.ts || '').replace('T',' ').slice(0,19)"></td>
|
||||
<td class="px-4 py-2" x-text="e.category"></td>
|
||||
<td class="px-4 py-2" x-text="e.action"></td>
|
||||
<td class="px-4 py-2"><span class="badge" :class="e.status==='ok' ? 'badge-ok' : 'badge-danger'" x-text="e.status"></span></td>
|
||||
<td class="px-4 py-2" x-text="auditNaam('cat', e.category)" :title="e.category"></td>
|
||||
<td class="px-4 py-2" x-text="auditNaam('act', e.action)" :title="e.action"></td>
|
||||
<td class="px-4 py-2"><span class="badge" :class="e.status==='ok' ? 'badge-ok' : 'badge-danger'" x-text="e.status === 'ok' ? t('ok') : t('failed')"></span></td>
|
||||
<td class="px-4 py-2 font-mono text-ac dark:text-ac-dark" x-text="e.ref || ''"></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -2244,7 +2315,7 @@ function app() {
|
|||
stacks: [],
|
||||
stackQuery: '', stackCat: '',
|
||||
storeQuery: '', storeTag: '', storeCat: '',
|
||||
filterOpen: false, filterTagQuery: '',
|
||||
filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false,
|
||||
categories: [], catMap: {},
|
||||
images: [],
|
||||
audit: [], auditTotal: 0, auditFacets: {}, auditTimer: null,
|
||||
|
|
@ -2334,6 +2405,22 @@ function app() {
|
|||
|
||||
// t('key') zoekt de vertaling op; t('key', {n: 3}) vervangt daarnaast
|
||||
// {n}-plaatshouders, zodat een zin niet in losse stukjes hoeft.
|
||||
// Het auditlog bewaart ruwe id's ('store', 'reconfigure'). Die horen niet
|
||||
// in beeld: vertaal ze waar we ze kennen, en val terug op de ruwe waarde
|
||||
// zodat een module met een eigen categorie niets kapotmaakt.
|
||||
auditNaam(soort, waarde) {
|
||||
if (!waarde) return '';
|
||||
return this.T[`audit_${soort}_${waarde}`] || waarde;
|
||||
},
|
||||
// De titel bovenaan. 'docker' heet 'images' in de vertalingen, en een
|
||||
// modulepagina heet 'mod:<id>' — die toonde letterlijk 'mod:mijnmodule'.
|
||||
get paginaTitel() {
|
||||
if (this.page.startsWith('mod:')) {
|
||||
const m = this.modules.find(x => 'mod:' + x.id === this.page);
|
||||
return m ? m.name : this.page.slice(4);
|
||||
}
|
||||
return this.t(this.page === 'docker' ? 'images' : this.page);
|
||||
},
|
||||
t(k, vars) {
|
||||
let s = this.T[k] || k;
|
||||
if (vars) for (const [naam, waarde] of Object.entries(vars)) {
|
||||
|
|
@ -2639,6 +2726,37 @@ function app() {
|
|||
return Object.keys(this.storeStacks || {})
|
||||
.reduce((n, id) => n + this.filteredStore(id).length, 0);
|
||||
},
|
||||
// Wat er op een telefoon onder de meer-knop komt te staan. Met namen
|
||||
// erbij: twaalf icoontjes zonder tekst zeggen niets.
|
||||
stackMenuItems(s) {
|
||||
return [
|
||||
{id:'update', icon:'mdi-update', label:this.t('update'),
|
||||
doe:() => this.stackAction(s.name, 'update')},
|
||||
{id:'env', icon:'mdi-file-cog-outline', label:'.env',
|
||||
doe:() => this.openEditor(s.name, 'env')},
|
||||
{id:'compose', icon:'mdi-file-code-outline', label:'compose',
|
||||
doe:() => this.openEditor(s.name, 'compose')},
|
||||
{id:'backup', icon:'mdi-archive-arrow-down-outline',label:this.t('backup'),
|
||||
doe:() => this.stackAction(s.name, 'backup')},
|
||||
{id:'creds', icon:'mdi-account-key-outline', label:this.t('credentials'),
|
||||
doe:() => this.openCredentials(s.name)},
|
||||
{id:'backups', icon:'mdi-history', label:this.t('backups'),
|
||||
doe:() => this.openBackups(s.name)},
|
||||
{id:'settings',icon:'mdi-tune', label:this.t('stack_settings'),
|
||||
uit: !s.editable, doe:() => this.openReconfigure(s.name)},
|
||||
{id:'connect', icon:s.connected ? 'mdi-lan-connect' : 'mdi-lan-pending',
|
||||
label:s.connected ? this.t('connected_to_apps') : this.t('connect_to_apps'),
|
||||
uit: !!s.connected, doe:() => this.connectStack(s.name)},
|
||||
{id:'remove', icon:'mdi-delete-outline', label:this.t('remove'),
|
||||
gevaar:true, doe:() => this.confirmRemove(s.name)},
|
||||
];
|
||||
},
|
||||
// Vanuit de store doorklikken naar de stack die er al draait.
|
||||
gaNaarStack(naam) {
|
||||
this.stackQuery = naam;
|
||||
this.stackCat = '';
|
||||
this.goto('stacks');
|
||||
},
|
||||
wisFilters() {
|
||||
this.storeCat = '';
|
||||
this.storeTag = '';
|
||||
|
|
|
|||
|
|
@ -358,5 +358,80 @@
|
|||
"clear_filters": "Clear",
|
||||
"more_tags": "and {n} more — type to search",
|
||||
"n_apps_found": "{n} apps",
|
||||
"clear": "Clear"
|
||||
"clear": "Clear",
|
||||
"audit_cat_audit": "Audit log",
|
||||
"audit_cat_auth": "Accounts",
|
||||
"audit_cat_backups": "Backups",
|
||||
"audit_cat_containers": "Containers",
|
||||
"audit_cat_core": "System",
|
||||
"audit_cat_docker": "Docker",
|
||||
"audit_cat_i18n": "Translations",
|
||||
"audit_cat_modules": "Modules",
|
||||
"audit_cat_networks": "Networks",
|
||||
"audit_cat_notify": "Notifications",
|
||||
"audit_cat_pangolin": "Pangolin",
|
||||
"audit_cat_repos": "Repositories",
|
||||
"audit_cat_stacks": "Stacks",
|
||||
"audit_cat_store": "App store",
|
||||
"audit_cat_update": "Updates",
|
||||
"audit_cat_wizard": "Wizard",
|
||||
"audit_act_add": "Added",
|
||||
"audit_act_app_create": "Network created",
|
||||
"audit_act_app_delete": "Network deleted",
|
||||
"audit_act_apply": "Applied",
|
||||
"audit_act_backup": "Backed up",
|
||||
"audit_act_boot_repo": "Repo at startup",
|
||||
"audit_act_boot_sync": "Sync at startup",
|
||||
"audit_act_clear": "Cleared",
|
||||
"audit_act_complete": "Completed",
|
||||
"audit_act_compose_edit": "Compose edited",
|
||||
"audit_act_connect": "Connected",
|
||||
"audit_act_create": "Created",
|
||||
"audit_act_credentials_view": "Credentials viewed",
|
||||
"audit_act_delete": "Deleted",
|
||||
"audit_act_down": "Stopped",
|
||||
"audit_act_download": "Downloaded",
|
||||
"audit_act_env_edit": ".env edited",
|
||||
"audit_act_git_add": "Git connected",
|
||||
"audit_act_install": "Installed",
|
||||
"audit_act_install_forgejo": "Forgejo installed",
|
||||
"audit_act_login": "Signed in",
|
||||
"audit_act_logout": "Signed out",
|
||||
"audit_act_mode": "Mode changed",
|
||||
"audit_act_offsite": "Copied offsite",
|
||||
"audit_act_password": "Password changed",
|
||||
"audit_act_prune": "Pruned",
|
||||
"audit_act_publish": "Published",
|
||||
"audit_act_pull": "Pulled",
|
||||
"audit_act_reconfigure": "Reconfigured",
|
||||
"audit_act_repos": "Repositories set",
|
||||
"audit_act_restart": "Restarted",
|
||||
"audit_act_restore": "Restored",
|
||||
"audit_act_rmi": "Image removed",
|
||||
"audit_act_rollback": "Rolled back",
|
||||
"audit_act_save": "Saved",
|
||||
"audit_act_scheduled": "Scheduled run",
|
||||
"audit_act_settings": "Settings changed",
|
||||
"audit_act_setup": "Account created",
|
||||
"audit_act_start": "Started",
|
||||
"audit_act_startup": "Started up",
|
||||
"audit_act_stop": "Stopped",
|
||||
"audit_act_test": "Test run",
|
||||
"audit_act_uninstall": "Uninstalled",
|
||||
"audit_act_up": "Started",
|
||||
"audit_act_update": "Updated",
|
||||
"audit_act_user_add": "User added",
|
||||
"audit_act_user_del": "User removed",
|
||||
"audit_act_user_role": "Role changed",
|
||||
"audit_act_verify": "Verified",
|
||||
"col_time": "Time",
|
||||
"col_category": "Category",
|
||||
"col_action": "Action",
|
||||
"col_status": "Status",
|
||||
"col_ref": "Subject",
|
||||
"open_stack": "Go to stack",
|
||||
"more": "More",
|
||||
"ok": "OK",
|
||||
"failed": "Failed",
|
||||
"install_again": "Install another"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,5 +358,80 @@
|
|||
"clear_filters": "Wissen",
|
||||
"more_tags": "en nog {n} andere — typ om te zoeken",
|
||||
"n_apps_found": "{n} apps",
|
||||
"clear": "Wissen"
|
||||
"clear": "Wissen",
|
||||
"audit_cat_audit": "Auditlog",
|
||||
"audit_cat_auth": "Accounts",
|
||||
"audit_cat_backups": "Backups",
|
||||
"audit_cat_containers": "Containers",
|
||||
"audit_cat_core": "Systeem",
|
||||
"audit_cat_docker": "Docker",
|
||||
"audit_cat_i18n": "Vertalingen",
|
||||
"audit_cat_modules": "Modules",
|
||||
"audit_cat_networks": "Netwerken",
|
||||
"audit_cat_notify": "Meldingen",
|
||||
"audit_cat_pangolin": "Pangolin",
|
||||
"audit_cat_repos": "Repositories",
|
||||
"audit_cat_stacks": "Stacks",
|
||||
"audit_cat_store": "App Store",
|
||||
"audit_cat_update": "Bijwerken",
|
||||
"audit_cat_wizard": "Wizard",
|
||||
"audit_act_add": "Toegevoegd",
|
||||
"audit_act_app_create": "Netwerk gemaakt",
|
||||
"audit_act_app_delete": "Netwerk verwijderd",
|
||||
"audit_act_apply": "Toegepast",
|
||||
"audit_act_backup": "Backup gemaakt",
|
||||
"audit_act_boot_repo": "Repo bij opstarten",
|
||||
"audit_act_boot_sync": "Sync bij opstarten",
|
||||
"audit_act_clear": "Gewist",
|
||||
"audit_act_complete": "Afgerond",
|
||||
"audit_act_compose_edit": "Compose bewerkt",
|
||||
"audit_act_connect": "Gekoppeld",
|
||||
"audit_act_create": "Aangemaakt",
|
||||
"audit_act_credentials_view": "Inloggegevens bekeken",
|
||||
"audit_act_delete": "Verwijderd",
|
||||
"audit_act_down": "Gestopt",
|
||||
"audit_act_download": "Gedownload",
|
||||
"audit_act_env_edit": ".env bewerkt",
|
||||
"audit_act_git_add": "Git gekoppeld",
|
||||
"audit_act_install": "Geïnstalleerd",
|
||||
"audit_act_install_forgejo": "Forgejo geïnstalleerd",
|
||||
"audit_act_login": "Ingelogd",
|
||||
"audit_act_logout": "Uitgelogd",
|
||||
"audit_act_mode": "Modus gewijzigd",
|
||||
"audit_act_offsite": "Extern weggezet",
|
||||
"audit_act_password": "Wachtwoord gewijzigd",
|
||||
"audit_act_prune": "Opgeruimd",
|
||||
"audit_act_publish": "Gepubliceerd",
|
||||
"audit_act_pull": "Opgehaald",
|
||||
"audit_act_reconfigure": "Opnieuw ingesteld",
|
||||
"audit_act_repos": "Repositories ingesteld",
|
||||
"audit_act_restart": "Herstart",
|
||||
"audit_act_restore": "Teruggezet",
|
||||
"audit_act_rmi": "Image verwijderd",
|
||||
"audit_act_rollback": "Teruggedraaid",
|
||||
"audit_act_save": "Opgeslagen",
|
||||
"audit_act_scheduled": "Volgens planning",
|
||||
"audit_act_settings": "Instellingen gewijzigd",
|
||||
"audit_act_setup": "Account aangemaakt",
|
||||
"audit_act_start": "Gestart",
|
||||
"audit_act_startup": "Opgestart",
|
||||
"audit_act_stop": "Gestopt",
|
||||
"audit_act_test": "Test uitgevoerd",
|
||||
"audit_act_uninstall": "Verwijderd",
|
||||
"audit_act_up": "Gestart",
|
||||
"audit_act_update": "Bijgewerkt",
|
||||
"audit_act_user_add": "Gebruiker toegevoegd",
|
||||
"audit_act_user_del": "Gebruiker verwijderd",
|
||||
"audit_act_user_role": "Rol gewijzigd",
|
||||
"audit_act_verify": "Gecontroleerd",
|
||||
"col_time": "Tijd",
|
||||
"col_category": "Categorie",
|
||||
"col_action": "Actie",
|
||||
"col_status": "Status",
|
||||
"col_ref": "Betreft",
|
||||
"open_stack": "Naar de stack",
|
||||
"more": "Meer",
|
||||
"ok": "Gelukt",
|
||||
"failed": "Mislukt",
|
||||
"install_again": "Nog een keer"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,42 @@ check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
|
|||
c.install.fields.push(veld('c','basis',{group:'Tweede'}));
|
||||
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
|
||||
|
||||
// ── Leesbare namen ──────────────────────────────────────────────────────────
|
||||
// Het auditlog bewaart ruwe id's; die horen niet in beeld te staan.
|
||||
c.T = {audit_cat_store: 'App Store', audit_act_reconfigure: 'Opnieuw ingesteld'};
|
||||
check('categorie wordt vertaald', c.auditNaam('cat', 'store') === 'App Store');
|
||||
check('actie wordt vertaald', c.auditNaam('act', 'reconfigure') === 'Opnieuw ingesteld');
|
||||
check('onbekende waarde blijft leesbaar zoals hij is',
|
||||
c.auditNaam('cat', 'eigenmodule') === 'eigenmodule');
|
||||
check('lege waarde geeft niets', c.auditNaam('act', '') === '');
|
||||
|
||||
c.modules = [{id: 'mijnmodule', name: 'Mijn Module'}];
|
||||
c.page = 'mod:mijnmodule';
|
||||
check('modulepagina toont de naam, niet mod:id', c.paginaTitel === 'Mijn Module');
|
||||
c.page = 'mod:onbekend';
|
||||
check('onbekende module valt terug op de id zonder voorvoegsel',
|
||||
c.paginaTitel === 'onbekend');
|
||||
c.T = {}; c.page = 'stacks';
|
||||
|
||||
// ── Menu per stack op de telefoon ───────────────────────────────────────────
|
||||
const acties = c.stackMenuItems({name: 's', editable: false, connected: true});
|
||||
check('het menu heeft alle acties', acties.length === 9);
|
||||
check('elke actie heeft een naam', acties.every(a => a.label && a.label.length));
|
||||
check('instellingen uit als de stack niet bewerkbaar is',
|
||||
acties.find(a => a.id === 'settings').uit === true);
|
||||
check('koppelen uit als hij al gekoppeld is',
|
||||
acties.find(a => a.id === 'connect').uit === true);
|
||||
check('verwijderen is als gevaarlijk gemarkeerd',
|
||||
acties.find(a => a.id === 'remove').gevaar === true);
|
||||
|
||||
// ── Vanuit de store naar een geïnstalleerde stack ───────────────────────────
|
||||
c.stackQuery = 'iets anders'; c.stackCat = 'media'; c.page = 'app_store';
|
||||
c.goto = (p) => { c.page = p; };
|
||||
c.gaNaarStack('mijn-sonarr');
|
||||
check('doorklikken zet de zoekterm', c.stackQuery === 'mijn-sonarr');
|
||||
check('doorklikken wist het categoriefilter', c.stackCat === '');
|
||||
check('doorklikken opent de stackspagina', c.page === 'stacks');
|
||||
|
||||
// ── Filtermenu van de app store ─────────────────────────────────────────────
|
||||
// De labelrij was afgekapt op veertien, waardoor je de rest niet kon kiezen. Het
|
||||
// menu zoekt nu in de volledige lijst.
|
||||
|
|
|
|||
|
|
@ -248,3 +248,57 @@ def test_preview_meldt_wat_er_nog_ontbreekt(client, env, monkeypatch):
|
|||
assert deps["mosquitto"]["stacks"] == ["mqtt"]
|
||||
assert deps["loki"]["installed"] is False
|
||||
assert deps["loki"]["reason"] == "logs"
|
||||
|
||||
|
||||
def test_store_herkent_een_installatie_aan_zijn_bron(env, tmp_path):
|
||||
"""De store keek naar de mapnaam: een stack telde als installatie van
|
||||
`sonarr` zodra hij `sonarr` of `sonarr-…` heette. Wie zijn stack `media-tv`
|
||||
noemde, kreeg te zien dat Sonarr nog niet geïnstalleerd was — terwijl hij
|
||||
draaide. `.serverup.json` weet wél uit welk sjabloon hij komt.
|
||||
"""
|
||||
import json as _json
|
||||
from core import git
|
||||
|
||||
sjablonen = tmp_path / "sjablonen"
|
||||
(sjablonen / "sonarr" / "files").mkdir(parents=True)
|
||||
(sjablonen / "sonarr" / "template.json").write_text(_json.dumps({
|
||||
"kind": "compose",
|
||||
"metadata": {"name": "Sonarr", "description": "d", "tags": ["media"]},
|
||||
"variables": [],
|
||||
}), encoding="utf-8")
|
||||
(sjablonen / "sonarr" / "files" / "compose.yaml").write_text(
|
||||
"services: {}\n", encoding="utf-8")
|
||||
|
||||
lib = env["lib"]
|
||||
# Een installatie met een eigen naam …
|
||||
eigen = lib / "media-tv"
|
||||
eigen.mkdir(parents=True)
|
||||
(eigen / ".serverup.json").write_text(_json.dumps({"source": "sonarr"}))
|
||||
# … en eentje die toevallig wel zo heet, zonder metadata.
|
||||
(lib / "sonarr-2").mkdir()
|
||||
|
||||
stacks = {s["dir"]: s for s in git._scan_compose_dirs(sjablonen, lib)}
|
||||
assert stacks["sonarr"]["instances"] == ["media-tv", "sonarr-2"]
|
||||
|
||||
|
||||
def test_store_ziet_geen_installatie_van_een_andere_app(env, tmp_path):
|
||||
import json as _json
|
||||
from core import git
|
||||
|
||||
sjablonen = tmp_path / "sjablonen"
|
||||
(sjablonen / "radarr" / "files").mkdir(parents=True)
|
||||
(sjablonen / "radarr" / "template.json").write_text(_json.dumps({
|
||||
"kind": "compose",
|
||||
"metadata": {"name": "Radarr", "description": "d", "tags": ["media"]},
|
||||
"variables": [],
|
||||
}), encoding="utf-8")
|
||||
(sjablonen / "radarr" / "files" / "compose.yaml").write_text(
|
||||
"services: {}\n", encoding="utf-8")
|
||||
|
||||
lib = env["lib"]
|
||||
d = lib / "media-tv"
|
||||
d.mkdir(parents=True)
|
||||
(d / ".serverup.json").write_text(_json.dumps({"source": "sonarr"}))
|
||||
|
||||
stacks = {s["dir"]: s for s in git._scan_compose_dirs(sjablonen, lib)}
|
||||
assert stacks["radarr"]["instances"] == []
|
||||
|
|
|
|||
|
|
@ -91,3 +91,40 @@ def test_i18n_module_leest_beide_talen(env):
|
|||
# strings() valt terug op het Engels voor ontbrekende sleutels.
|
||||
assert i18n.strings("nl")["login"]
|
||||
assert i18n.strings("en")["login"]
|
||||
|
||||
|
||||
def test_elke_auditcategorie_heeft_een_nederlandse_naam():
|
||||
"""Het auditlog toonde ruwe id's als 'store' en 'reconfigure'.
|
||||
|
||||
Die worden nu vertaald met `audit_cat_<x>` en `audit_act_<x>`. Voegt iemand
|
||||
een nieuwe `audit.log(...)` toe zonder vertaling, dan staat er weer een ruwe
|
||||
id in beeld — vandaar dat deze test de broncode afloopt.
|
||||
"""
|
||||
import re
|
||||
nl = _laad("nl")
|
||||
bron = ""
|
||||
for pad in WORTEL.rglob("*.py"):
|
||||
bron += pad.read_text(encoding="utf-8")
|
||||
|
||||
# audit.log("categorie", "actie", ...) — alleen de letterlijke waarden;
|
||||
# aanroepen met een variabele (containers/stacks) staan hieronder apart.
|
||||
paren = re.findall(r'audit\.log\(\s*"([a-z0-9_]+)"\s*,\s*"([a-z0-9_]+)"', bron)
|
||||
assert paren, "geen audit.log-aanroepen gevonden — is de vorm veranderd?"
|
||||
|
||||
ontbreekt = []
|
||||
for cat, act in set(paren):
|
||||
if f"audit_cat_{cat}" not in nl:
|
||||
ontbreekt.append(f"audit_cat_{cat}")
|
||||
if f"audit_act_{act}" not in nl:
|
||||
ontbreekt.append(f"audit_act_{act}")
|
||||
# De acties die als variabele worden meegegeven: stack- en containerknoppen.
|
||||
for act in ("start", "stop", "restart", "up", "down", "update", "backup"):
|
||||
if f"audit_act_{act}" not in nl:
|
||||
ontbreekt.append(f"audit_act_{act}")
|
||||
assert not ontbreekt, f"geen vertaling voor: {sorted(set(ontbreekt))}"
|
||||
|
||||
|
||||
def test_auditnamen_bestaan_in_beide_talen():
|
||||
nl, en = _laad("nl"), _laad("en")
|
||||
alleen_nl = sorted(k for k in nl if k.startswith("audit_") and k not in en)
|
||||
assert not alleen_nl, f"ontbreken in en.json: {alleen_nl}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue