fix: herstel app verwijderen en mobiele modals
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m46s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m46s
This commit is contained in:
parent
1575db42c2
commit
6c84def759
9 changed files with 302 additions and 56 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -1,3 +1,25 @@
|
||||||
|
# v0.8.15-beta — Apps verwijderen en mobiele schermen werken weer
|
||||||
|
|
||||||
|
**De verwijderknop stopte containers en verwijderde volumes, maar liet de
|
||||||
|
appconfiguratie staan.** Daardoor bleef de app in Server Up zichtbaar en leek de
|
||||||
|
knop niets te doen. Daarnaast konden modals en instellingen op een klein scherm
|
||||||
|
buiten de viewport vallen zonder dat de inhoud bereikbaar was.
|
||||||
|
|
||||||
|
- **Verwijderen doet nu wat de keuze zegt.** De veilige keuze verwijdert de
|
||||||
|
appconfiguratie, maar bewaart appdata en named volumes. De expliciete keuze
|
||||||
|
“app en alle data verwijderen” wist ook appdata en niet-externe volumes.
|
||||||
|
- **Overgenomen apps zijn verwijderbaar.** Bij een app die als symlink is
|
||||||
|
gekoppeld wordt alleen de Server Up-verwijzing weggehaald; de externe
|
||||||
|
Compose-map van de gebruiker blijft bestaan.
|
||||||
|
- **Duidelijke voortgang en fouten.** De dialoog onthoudt de juiste app,
|
||||||
|
voorkomt dubbele opdrachten en toont zowel succes als backendfouten.
|
||||||
|
- **Modals passen op ieder mobiel scherm.** Dialogen en de setupwizard gebruiken
|
||||||
|
de dynamische viewporthoogte, houden kop en knoppen in beeld en maken de
|
||||||
|
inhoud zelfstandig scrollbaar.
|
||||||
|
- **Instellingen blijven binnen de schermbreedte.** Brede keuzerijen kunnen
|
||||||
|
horizontaal scrollen, kaartkoppen lopen om en invoervelden mogen binnen hun
|
||||||
|
grid krimpen. Ook het loginvenster blijft op een laag scherm bereikbaar.
|
||||||
|
|
||||||
# v0.8.14-beta — Raspberry Pi krijgt werkende en eerlijke apps
|
# v0.8.14-beta — Raspberry Pi krijgt werkende en eerlijke apps
|
||||||
|
|
||||||
**De UniFi-template startte een MongoDB-container, maar maakte de database-
|
**De UniFi-template startte een MongoDB-container, maar maakte de database-
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.8.14-beta
|
0.8.15-beta
|
||||||
|
|
|
||||||
|
|
@ -752,7 +752,15 @@ def _stack_dir(name: str) -> tuple[Path | None, Path | None]:
|
||||||
if not cfg.safe_name(name):
|
if not cfg.safe_name(name):
|
||||||
return None, None
|
return None, None
|
||||||
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
lib = Path(cfg.load()["LIBRARY_DIR"]).resolve()
|
||||||
d = (lib / name).resolve()
|
kandidaat = lib / name
|
||||||
|
# Overgenomen compose-apps zijn bewust een directe symlink in LIBRARY_DIR.
|
||||||
|
# resolve() maakte daarvan een pad buiten de library en blokkeerde daarna
|
||||||
|
# alle acties, inclusief het loskoppelen waarvoor hieronder speciale code
|
||||||
|
# staat. De veilige naam garandeert dat alleen dit ene directe kind wordt
|
||||||
|
# gekozen; een gewone map blijft volledig geresolved en begrensd.
|
||||||
|
if kandidaat.is_symlink():
|
||||||
|
return lib, kandidaat
|
||||||
|
d = kandidaat.resolve()
|
||||||
if not d.is_relative_to(lib) or d == lib:
|
if not d.is_relative_to(lib) or d == lib:
|
||||||
return None, None
|
return None, None
|
||||||
return lib, d
|
return lib, d
|
||||||
|
|
@ -763,17 +771,20 @@ def api_stack_action(name, action):
|
||||||
lib, d = _stack_dir(name)
|
lib, d = _stack_dir(name)
|
||||||
if not d:
|
if not d:
|
||||||
return jsonify(ok=False, msg="ongeldige naam"), 400
|
return jsonify(ok=False, msg="ongeldige naam"), 400
|
||||||
if not d.exists():
|
if not d.exists() and not (action == "remove" and d.is_symlink()):
|
||||||
return jsonify(ok=False, msg="niet gevonden"), 404
|
return jsonify(ok=False, msg="niet gevonden"), 404
|
||||||
if action not in ("start", "stop", "restart", "update", "remove", "backup"):
|
if action not in ("start", "stop", "restart", "update", "remove", "backup"):
|
||||||
return jsonify(ok=False, msg="onbekend"), 400
|
return jsonify(ok=False, msg="onbekend"), 400
|
||||||
body = request.json or {}
|
body = request.json or {}
|
||||||
|
remove_mode = body.get("mode", "full")
|
||||||
|
if action == "remove" and remove_mode not in ("stack", "full", "all"):
|
||||||
|
return jsonify(ok=False, msg="ongeldige verwijderkeuze"), 400
|
||||||
|
|
||||||
jid, q = jobs.create(f"{action}:{name}")
|
jid, q = jobs.create(f"{action}:{name}")
|
||||||
|
|
||||||
def _run(jid):
|
def _run(jid):
|
||||||
qq = jobs.get_queue(jid)
|
qq = jobs.get_queue(jid)
|
||||||
pdir = Path(cfg.load()["LIBRARY_DIR"]).resolve() / name
|
pdir = d
|
||||||
lf = lambda m: jobs.log(qq, "dim", m)
|
lf = lambda m: jobs.log(qq, "dim", m)
|
||||||
try:
|
try:
|
||||||
jobs.log(qq, "section", f"{action}: {name}")
|
jobs.log(qq, "section", f"{action}: {name}")
|
||||||
|
|
@ -790,27 +801,35 @@ def api_stack_action(name, action):
|
||||||
docker.compose_pull(pdir, log_fn=lf)
|
docker.compose_pull(pdir, log_fn=lf)
|
||||||
rc = docker.compose_up(pdir, log_fn=lf, name=name)
|
rc = docker.compose_up(pdir, log_fn=lf, name=name)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
mode = body.get("mode", "stack")
|
mode = remove_mode
|
||||||
if mode in ("full", "all"):
|
if mode in ("full", "all"):
|
||||||
_auto_backup(qq, name, "BACKUP_BEFORE_REMOVE", "voor-remove")
|
_auto_backup(qq, name, "BACKUP_BEFORE_REMOVE", "voor-remove")
|
||||||
if docker.has_compose(pdir):
|
if docker.has_compose(pdir):
|
||||||
docker.compose_down(pdir, log_fn=lf, volumes=True, name=name)
|
# De gewone verwijderactie bewaart data: dus ook named
|
||||||
if mode in ("full", "all") and pdir.is_dir():
|
# volumes. Alleen de expliciete alles-wissenkeuze gebruikt
|
||||||
|
# compose down -v. De oude API-modus `stack` behoudt zijn
|
||||||
|
# historische betekenis (alleen stoppen + volumes wissen).
|
||||||
|
docker.compose_down(
|
||||||
|
pdir, log_fn=lf, volumes=mode in ("stack", "all"),
|
||||||
|
name=name)
|
||||||
|
if mode in ("full", "all") and (
|
||||||
|
pdir.is_dir() or pdir.is_symlink()):
|
||||||
# Een overgenomen app is een symlink naar een map van de
|
# Een overgenomen app is een symlink naar een map van de
|
||||||
# gebruiker zelf. Die map is niet van ons; alleen de
|
# gebruiker zelf. Die map is niet van ons; alleen de
|
||||||
# verwijzing halen we weg. `shutil.rmtree` zou hier
|
# verwijzing halen we weg.
|
||||||
# bovendien weigeren ("Cannot call rmtree on a symbolic
|
|
||||||
# link") en de verwijdering halverwege laten stranden.
|
|
||||||
if pdir.is_symlink():
|
if pdir.is_symlink():
|
||||||
doelmap = os.path.realpath(pdir)
|
doelmap = os.path.realpath(pdir)
|
||||||
pdir.unlink()
|
pdir.unlink()
|
||||||
jobs.log(qq, "ok", f"Losgekoppeld; {doelmap} blijft staan")
|
jobs.log(qq, "ok", f"Losgekoppeld; {doelmap} blijft staan")
|
||||||
else:
|
else:
|
||||||
shutil.rmtree(pdir)
|
shutil.rmtree(pdir)
|
||||||
jobs.log(qq, "ok", "Bestanden verwijderd")
|
jobs.log(qq, "ok", "Appbestanden verwijderd")
|
||||||
if mode == "all":
|
if mode == "all":
|
||||||
dd = Path(cfg.load()["DATA_DIR"]).resolve() / name
|
dd = Path(cfg.load()["DATA_DIR"]).resolve() / name
|
||||||
if dd.is_dir():
|
if dd.is_symlink():
|
||||||
|
dd.unlink()
|
||||||
|
jobs.log(qq, "ok", "Dataverwijzing verwijderd")
|
||||||
|
elif dd.is_dir():
|
||||||
shutil.rmtree(dd)
|
shutil.rmtree(dd)
|
||||||
jobs.log(qq, "ok", "Data verwijderd")
|
jobs.log(qq, "ok", "Data verwijderd")
|
||||||
elif action == "backup":
|
elif action == "backup":
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ tailwind.config = {
|
||||||
en JetBrains Mono worden gebruikt als ze lokaal geïnstalleerd zijn, anders
|
en JetBrains Mono worden gebruikt als ze lokaal geïnstalleerd zijn, anders
|
||||||
valt de stack terug op de systeemfonts uit tailwind.config. -->
|
valt de stack terug op de systeemfonts uit tailwind.config. -->
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; height: 100vh; height: 100dvh; }
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
.mdi-spin { animation: mdis 1s linear infinite; display:inline-block; }
|
.mdi-spin { animation: mdis 1s linear infinite; display:inline-block; }
|
||||||
@keyframes mdis { to { transform: rotate(360deg); } }
|
@keyframes mdis { to { transform: rotate(360deg); } }
|
||||||
|
|
@ -70,7 +70,7 @@ tailwind.config = {
|
||||||
<!-- Ligt bovenop alles (z-[90]) tot er een geldige sessie is. De API weigert
|
<!-- Ligt bovenop alles (z-[90]) tot er een geldige sessie is. De API weigert
|
||||||
ondertussen sowieso elk verzoek met 401, dit is de nette voorkant. -->
|
ondertussen sowieso elk verzoek met 401, dit is de nette voorkant. -->
|
||||||
<div x-show="auth.ready && !auth.authenticated" x-cloak
|
<div x-show="auth.ready && !auth.authenticated" x-cloak
|
||||||
class="fixed inset-0 bg-bg dark:bg-bg-dark z-[90] grid place-items-center p-4">
|
class="fixed inset-0 bg-bg dark:bg-bg-dark z-[90] grid place-items-center p-4 overflow-y-auto">
|
||||||
<div class="w-full max-w-[380px] anim">
|
<div class="w-full max-w-[380px] anim">
|
||||||
<div class="flex items-center gap-3 mb-6 justify-center">
|
<div class="flex items-center gap-3 mb-6 justify-center">
|
||||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-ac to-ac2 grid place-items-center shadow-soft text-white text-xl">🚀</div>
|
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-ac to-ac2 grid place-items-center shadow-soft text-white text-xl">🚀</div>
|
||||||
|
|
@ -194,7 +194,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Scroll area -->
|
<!-- Scroll area -->
|
||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="p-4 sm:p-7 max-w-6xl mx-auto pb-48">
|
<div class="p-4 sm:p-7 max-w-6xl mx-auto pb-48 min-w-0">
|
||||||
|
|
||||||
<!-- ══ Dashboard ══ -->
|
<!-- ══ Dashboard ══ -->
|
||||||
<section x-show="page === 'dashboard'" x-cloak class="anim">
|
<section x-show="page === 'dashboard'" x-cloak class="anim">
|
||||||
|
|
@ -816,7 +816,7 @@ tailwind.config = {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══ Settings ══ -->
|
<!-- ══ Settings ══ -->
|
||||||
<section x-show="page === 'settings'" x-cloak class="anim space-y-5">
|
<section x-show="page === 'settings'" x-cloak class="anim space-y-5 min-w-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<p class="text-sm text-t2 dark:text-t2-dark flex-1" x-text="t('settings')">Instellingen</p>
|
<p class="text-sm text-t2 dark:text-t2-dark flex-1" x-text="t('settings')">Instellingen</p>
|
||||||
<span x-show="saved" class="text-xs text-ok flex items-center gap-1.5 font-semibold"><span class="mdi mdi-check-circle"></span> <span x-text="t('auto_saved')">Opgeslagen</span></span>
|
<span x-show="saved" class="text-xs text-ok flex items-center gap-1.5 font-semibold"><span class="mdi mdi-check-circle"></span> <span x-text="t('auto_saved')">Opgeslagen</span></span>
|
||||||
|
|
@ -825,7 +825,7 @@ tailwind.config = {
|
||||||
<div class="card card-pad grid sm:grid-cols-2 gap-6">
|
<div class="card card-pad grid sm:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="form-label" x-text="t('theme')">Thema</div>
|
<div class="form-label" x-text="t('theme')">Thema</div>
|
||||||
<div class="inline-flex rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1">
|
<div class="inline-flex max-w-full overflow-x-auto rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1">
|
||||||
<button @click="setThemePref('system')" class="seg" :class="themePref==='system' ? 'seg-on' : ''"><span class="mdi mdi-monitor"></span> Auto</button>
|
<button @click="setThemePref('system')" class="seg" :class="themePref==='system' ? 'seg-on' : ''"><span class="mdi mdi-monitor"></span> Auto</button>
|
||||||
<button @click="setThemePref('light')" class="seg" :class="themePref==='light' ? 'seg-on' : ''"><span class="mdi mdi-weather-sunny"></span> Light</button>
|
<button @click="setThemePref('light')" class="seg" :class="themePref==='light' ? 'seg-on' : ''"><span class="mdi mdi-weather-sunny"></span> Light</button>
|
||||||
<button @click="setThemePref('dark')" class="seg" :class="themePref==='dark' ? 'seg-on' : ''"><span class="mdi mdi-weather-night"></span> Dark</button>
|
<button @click="setThemePref('dark')" class="seg" :class="themePref==='dark' ? 'seg-on' : ''"><span class="mdi mdi-weather-night"></span> Dark</button>
|
||||||
|
|
@ -841,7 +841,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Setup wizard opnieuw doorlopen -->
|
<!-- Setup wizard opnieuw doorlopen -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-wizard-hat text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-wizard-hat text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('wizard')">Setup Wizard</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('wizard')">Setup Wizard</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -858,10 +858,10 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Beveiliging: accounts + wachtwoord -->
|
<!-- Beveiliging: accounts + wachtwoord -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-shield-account-outline text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-shield-account-outline text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('security')">Beveiliging</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('security')">Beveiliging</span>
|
||||||
<span class="badge badge-dim ml-auto" x-text="t('logged_in_as', {user: auth.user})"></span>
|
<span class="badge badge-dim sm:ml-auto max-w-full truncate" x-text="t('logged_in_as', {user: auth.user})"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5 grid md:grid-cols-2 gap-6">
|
<div class="p-5 grid md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -907,7 +907,7 @@ tailwind.config = {
|
||||||
adressen hieronder; zonder die lijst weigert de server de modus. -->
|
adressen hieronder; zonder die lijst weigert de server de modus. -->
|
||||||
<div class="px-5 pb-5 pt-4 border-t border-bd dark:border-bd-dark">
|
<div class="px-5 pb-5 pt-4 border-t border-bd dark:border-bd-dark">
|
||||||
<div class="form-label" x-text="t('auth_mode')">Hoe wordt er ingelogd?</div>
|
<div class="form-label" x-text="t('auth_mode')">Hoe wordt er ingelogd?</div>
|
||||||
<div class="inline-flex rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1 mb-2">
|
<div class="inline-flex max-w-full overflow-x-auto rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1 mb-2">
|
||||||
<button class="seg" :class="authCfg.mode==='local' ? 'seg-on' : ''"
|
<button class="seg" :class="authCfg.mode==='local' ? 'seg-on' : ''"
|
||||||
@click="authCfg.mode='local'"><span class="mdi mdi-account-key-outline"></span> <span x-text="t('auth_mode_local')">Lokaal</span></button>
|
@click="authCfg.mode='local'"><span class="mdi mdi-account-key-outline"></span> <span x-text="t('auth_mode_local')">Lokaal</span></button>
|
||||||
<button class="seg" :class="authCfg.mode==='proxy' ? 'seg-on' : ''"
|
<button class="seg" :class="authCfg.mode==='proxy' ? 'seg-on' : ''"
|
||||||
|
|
@ -944,7 +944,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Backups: bewaarbeleid en planning -->
|
<!-- Backups: bewaarbeleid en planning -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-archive-outline text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-archive-outline text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('backups')">Backups</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('backups')">Backups</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1018,7 +1018,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Meldingen: je hoort het als er 's nachts iets misgaat -->
|
<!-- Meldingen: je hoort het als er 's nachts iets misgaat -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-bell-outline text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-bell-outline text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('notifications')">Meldingen</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('notifications')">Meldingen</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1113,7 +1113,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Pangolin: publieke URL's zonder poorten open te zetten -->
|
<!-- Pangolin: publieke URL's zonder poorten open te zetten -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-earth text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-earth text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm">Pangolin</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm">Pangolin</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1162,7 +1162,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Netwerken: stacks een eigen IP-adres geven -->
|
<!-- Netwerken: stacks een eigen IP-adres geven -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-lan text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-lan text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('networks')">Netwerken</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('networks')">Netwerken</span>
|
||||||
<button class="btn-sm ml-auto" @click="modal='add-network'"><span class="mdi mdi-plus"></span> <span x-text="t('network')">Netwerk</span></button>
|
<button class="btn-sm ml-auto" @click="modal='add-network'"><span class="mdi mdi-plus"></span> <span x-text="t('network')">Netwerk</span></button>
|
||||||
|
|
@ -1187,7 +1187,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Updates -->
|
<!-- Updates -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap 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="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>
|
<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.update_available"><span class="badge badge-ok" x-text="t('update_available')">update beschikbaar</span></template>
|
||||||
|
|
@ -1231,7 +1231,7 @@ tailwind.config = {
|
||||||
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
|
<div class="pt-2 border-t border-bd dark:border-bd-dark space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label" x-text="t('channel')">Kanaal</label>
|
<label class="form-label" x-text="t('channel')">Kanaal</label>
|
||||||
<div class="inline-flex rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1">
|
<div class="inline-flex max-w-full overflow-x-auto rounded-xl bg-s2 dark:bg-s2-dark p-1 gap-1">
|
||||||
<button class="seg" :class="updChannel()==='stable' ? 'seg-on' : ''"
|
<button class="seg" :class="updChannel()==='stable' ? 'seg-on' : ''"
|
||||||
@click="setChannel('stable')"><span class="mdi mdi-check-decagram"></span> Stable</button>
|
@click="setChannel('stable')"><span class="mdi mdi-check-decagram"></span> Stable</button>
|
||||||
<button class="seg" :class="updChannel()==='beta' ? 'seg-on' : ''"
|
<button class="seg" :class="updChannel()==='beta' ? 'seg-on' : ''"
|
||||||
|
|
@ -1278,7 +1278,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Taalmodule (add-on) -->
|
<!-- Taalmodule (add-on) -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="px-5 py-4 flex items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
<div class="px-5 py-4 flex flex-wrap items-center gap-2.5 border-b border-bd dark:border-bd-dark">
|
||||||
<span class="mdi mdi-translate text-ac dark:text-ac-dark text-lg"></span>
|
<span class="mdi mdi-translate text-ac dark:text-ac-dark text-lg"></span>
|
||||||
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('language_addon')">Taalmodule</span>
|
<span class="font-bold text-wh dark:text-wh-dark text-sm" x-text="t('language_addon')">Taalmodule</span>
|
||||||
<span class="badge badge-info" x-text="languages.length"></span>
|
<span class="badge badge-info" x-text="languages.length"></span>
|
||||||
|
|
@ -1453,7 +1453,7 @@ tailwind.config = {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ░░ Modals ░░ -->
|
<!-- ░░ Modals ░░ -->
|
||||||
<div x-show="modal" @click.self="modal=null" x-cloak class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[60] grid place-items-center p-0 sm:p-4">
|
<div x-show="modal" @click.self="modal=null" x-cloak class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[60] grid place-items-center p-0 sm:p-4 overflow-hidden">
|
||||||
|
|
||||||
<!-- Add repo -->
|
<!-- Add repo -->
|
||||||
<div x-show="modal==='add-repo'" class="modal max-w-md">
|
<div x-show="modal==='add-repo'" class="modal max-w-md">
|
||||||
|
|
@ -2207,15 +2207,38 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- Remove confirm -->
|
<!-- Remove confirm -->
|
||||||
<div x-show="modal==='remove'" class="modal max-w-md">
|
<div x-show="modal==='remove'" class="modal max-w-md">
|
||||||
<div class="modal-head"><span class="mdi mdi-delete-alert-outline text-er"></span> <span x-text="t('remove_stack')">Verwijderen</span></div>
|
<div class="modal-head">
|
||||||
|
<span class="mdi mdi-delete-alert-outline text-er"></span>
|
||||||
|
<span x-text="t('remove_stack')">App verwijderen</span>
|
||||||
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p class="text-sm mb-4 text-t2 dark:text-t2-dark">App: <strong class="text-wh dark:text-wh-dark" x-text="install.stack"></strong></p>
|
<p class="text-sm mb-4 text-t2 dark:text-t2-dark">App:
|
||||||
<div class="flex flex-col gap-2">
|
<strong class="text-wh dark:text-wh-dark break-all" x-text="removeTarget"></strong>
|
||||||
<button class="btn" @click="doRemove('stack')"><span class="mdi mdi-stop-circle-outline"></span> <span x-text="t('remove_stack_only')">Stop + volumes</span></button>
|
</p>
|
||||||
<button class="btn btn-danger" @click="doRemove('all')"><span class="mdi mdi-delete-outline"></span> <span x-text="t('remove_all')">Verwijder alles</span></button>
|
<div class="flex flex-col gap-3">
|
||||||
|
<button class="btn !h-auto !py-3 !justify-start text-left"
|
||||||
|
:disabled="removeBusy" @click="doRemove('full')">
|
||||||
|
<span class="mdi mdi-delete-outline text-lg"></span>
|
||||||
|
<span class="min-w-0">
|
||||||
|
<span class="block" x-text="t('remove_stack_only')">App verwijderen · data bewaren</span>
|
||||||
|
<span class="block mt-0.5 text-[11px] font-normal text-dm dark:text-dm-dark"
|
||||||
|
x-text="t('remove_keep_data_hint')"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger !h-auto !py-3 !justify-start text-left"
|
||||||
|
:disabled="removeBusy" @click="doRemove('all')">
|
||||||
|
<span class="mdi mdi-delete-forever-outline text-lg"></span>
|
||||||
|
<span class="min-w-0">
|
||||||
|
<span class="block" x-text="t('remove_all')">App en alle data verwijderen</span>
|
||||||
|
<span class="block mt-0.5 text-[11px] font-normal opacity-80"
|
||||||
|
x-text="t('remove_all_hint')"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot"><button class="btn" @click="modal=null" x-text="t('cancel')">Annuleren</button></div>
|
<div class="modal-foot">
|
||||||
|
<button class="btn" :disabled="removeBusy" @click="modal=null" x-text="t('cancel')">Annuleren</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editor -->
|
<!-- Editor -->
|
||||||
|
|
@ -2225,7 +2248,7 @@ tailwind.config = {
|
||||||
<span x-text="editor.stack"></span>
|
<span x-text="editor.stack"></span>
|
||||||
<!-- Tabbladen: tot nu toe moest je de modal sluiten en opnieuw openen om
|
<!-- Tabbladen: tot nu toe moest je de modal sluiten en opnieuw openen om
|
||||||
van compose naar .env te gaan. -->
|
van compose naar .env te gaan. -->
|
||||||
<div class="ml-auto flex items-center gap-1">
|
<div class="ml-auto max-w-full flex items-center gap-1 overflow-x-auto">
|
||||||
<button class="btn-sm" :class="editor.type==='compose' ? '!bg-ac !text-white' : ''"
|
<button class="btn-sm" :class="editor.type==='compose' ? '!bg-ac !text-white' : ''"
|
||||||
@click="switchEditor('compose')">docker-compose.yml</button>
|
@click="switchEditor('compose')">docker-compose.yml</button>
|
||||||
<button class="btn-sm" :class="editor.type==='env' ? '!bg-ac !text-white' : ''"
|
<button class="btn-sm" :class="editor.type==='env' ? '!bg-ac !text-white' : ''"
|
||||||
|
|
@ -2344,10 +2367,10 @@ tailwind.config = {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ░░ First-run wizard ░░ -->
|
<!-- ░░ First-run wizard ░░ -->
|
||||||
<div x-show="wizard.open" x-cloak class="fixed inset-0 bg-slate-900/80 backdrop-blur z-[80] grid place-items-center p-4">
|
<div x-show="wizard.open" x-cloak class="fixed inset-0 bg-slate-900/80 backdrop-blur z-[80] grid place-items-center p-0 sm:p-4 overflow-hidden">
|
||||||
<div class="bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl w-full max-w-xl shadow-pop overflow-hidden">
|
<div class="modal max-w-xl">
|
||||||
<div class="h-1.5 bg-s2 dark:bg-s2-dark"><div class="h-full bg-gradient-to-r from-ac to-ac2 transition-all" :style="`width:${(wizard.step+1)/(wizard.steps.length||1)*100}%`"></div></div>
|
<div class="h-1.5 bg-s2 dark:bg-s2-dark"><div class="h-full bg-gradient-to-r from-ac to-ac2 transition-all" :style="`width:${(wizard.step+1)/(wizard.steps.length||1)*100}%`"></div></div>
|
||||||
<div class="p-7 min-h-[270px]">
|
<div class="modal-body !p-5 sm:!p-7 min-h-[270px]">
|
||||||
<h2 class="text-2xl font-extrabold text-wh dark:text-wh-dark mb-1 tracking-tight" x-text="wizard.steps[wizard.step]?.title || ''"></h2>
|
<h2 class="text-2xl font-extrabold text-wh dark:text-wh-dark mb-1 tracking-tight" x-text="wizard.steps[wizard.step]?.title || ''"></h2>
|
||||||
<p class="text-sm text-t2 dark:text-t2-dark mb-6" x-text="wizard.steps[wizard.step]?.subtitle || ''"></p>
|
<p class="text-sm text-t2 dark:text-t2-dark mb-6" x-text="wizard.steps[wizard.step]?.subtitle || ''"></p>
|
||||||
<div x-show="wizardStapId() === 'welkom'">
|
<div x-show="wizardStapId() === 'welkom'">
|
||||||
|
|
@ -2453,7 +2476,7 @@ tailwind.config = {
|
||||||
|
|
||||||
<div x-show="wizardStapId() === 'klaar'"><p class="text-sm">We synchroniseren nu de repos en zijn klaar voor gebruik.</p></div>
|
<div x-show="wizardStapId() === 'klaar'"><p class="text-sm">We synchroniseren nu de repos en zijn klaar voor gebruik.</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-7 py-4 border-t border-bd dark:border-bd-dark flex justify-end gap-2">
|
<div class="modal-foot !px-5 sm:!px-7">
|
||||||
<button class="btn" @click="wizard.open = false" x-text="t('close')">Sluiten</button>
|
<button class="btn" @click="wizard.open = false" x-text="t('close')">Sluiten</button>
|
||||||
<button class="btn" @click="wizard.step = Math.max(0, wizard.step-1)" :disabled="wizard.step===0" x-text="t('prev')">Terug</button>
|
<button class="btn" @click="wizard.step = Math.max(0, wizard.step-1)" :disabled="wizard.step===0" x-text="t('prev')">Terug</button>
|
||||||
<button class="btn btn-primary" @click="wizardNext()"><span x-text="wizard.step < wizard.steps.length-1 ? 'Volgende' : 'Klaar'"></span></button>
|
<button class="btn btn-primary" @click="wizardNext()"><span x-text="wizard.step < wizard.steps.length-1 ? 'Volgende' : 'Klaar'"></span></button>
|
||||||
|
|
@ -2472,7 +2495,7 @@ tailwind.config = {
|
||||||
.btn-sm { @apply inline-flex items-center justify-center gap-1.5 px-3 h-9 rounded-lg border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-tx dark:text-tx-dark text-xs font-semibold hover:bg-s2 dark:hover:bg-s2-dark hover:border-bd2 dark:hover:border-bd2-dark active:scale-95 cursor-pointer transition shrink-0; }
|
.btn-sm { @apply inline-flex items-center justify-center gap-1.5 px-3 h-9 rounded-lg border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-tx dark:text-tx-dark text-xs font-semibold hover:bg-s2 dark:hover:bg-s2-dark hover:border-bd2 dark:hover:border-bd2-dark active:scale-95 cursor-pointer transition shrink-0; }
|
||||||
.seg { @apply inline-flex items-center gap-1.5 px-3 h-9 rounded-lg text-xs font-semibold text-t2 dark:text-t2-dark cursor-pointer transition; }
|
.seg { @apply inline-flex items-center gap-1.5 px-3 h-9 rounded-lg text-xs font-semibold text-t2 dark:text-t2-dark cursor-pointer transition; }
|
||||||
.seg-on { @apply bg-s1 dark:bg-s1-dark text-ac dark:text-ac-dark shadow-soft; }
|
.seg-on { @apply bg-s1 dark:bg-s1-dark text-ac dark:text-ac-dark shadow-soft; }
|
||||||
.card { @apply bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl shadow-soft; }
|
.card { @apply min-w-0 bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl shadow-soft; }
|
||||||
.card-hover { @apply hover:shadow-soft2 hover:-translate-y-0.5 transition duration-200; }
|
.card-hover { @apply hover:shadow-soft2 hover:-translate-y-0.5 transition duration-200; }
|
||||||
.card-pad { @apply p-5; }
|
.card-pad { @apply p-5; }
|
||||||
.nav-link { @apply flex items-center gap-3 px-3.5 h-11 rounded-xl text-sm font-semibold text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark hover:text-tx dark:hover:text-tx-dark cursor-pointer transition mb-0.5; }
|
.nav-link { @apply flex items-center gap-3 px-3.5 h-11 rounded-xl text-sm font-semibold text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark hover:text-tx dark:hover:text-tx-dark cursor-pointer transition mb-0.5; }
|
||||||
|
|
@ -2482,7 +2505,7 @@ tailwind.config = {
|
||||||
.stat-value { @apply text-2xl sm:text-3xl font-extrabold text-wh dark:text-wh-dark leading-none tracking-tight; }
|
.stat-value { @apply text-2xl sm:text-3xl font-extrabold text-wh dark:text-wh-dark leading-none tracking-tight; }
|
||||||
.stat-label { @apply text-[11px] text-t2 dark:text-t2-dark mt-1.5 uppercase tracking-wider font-semibold truncate; }
|
.stat-label { @apply text-[11px] text-t2 dark:text-t2-dark mt-1.5 uppercase tracking-wider font-semibold truncate; }
|
||||||
.form-label { @apply block text-[11px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1.5; }
|
.form-label { @apply block text-[11px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1.5; }
|
||||||
.form-input { @apply w-full bg-s1 dark:bg-s2-dark border border-bd dark:border-bd-dark rounded-xl px-3.5 h-11 text-sm text-tx dark:text-tx-dark outline-none focus:border-ac focus:ring-4 focus:ring-ac/10 transition placeholder:text-dm dark:placeholder:text-dm-dark; }
|
.form-input { @apply min-w-0 w-full bg-s1 dark:bg-s2-dark border border-bd dark:border-bd-dark rounded-xl px-3.5 h-11 text-sm text-tx dark:text-tx-dark outline-none focus:border-ac focus:ring-4 focus:ring-ac/10 transition placeholder:text-dm dark:placeholder:text-dm-dark; }
|
||||||
textarea.form-input { @apply h-auto py-2.5; }
|
textarea.form-input { @apply h-auto py-2.5; }
|
||||||
.badge { @apply inline-flex items-center text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-md; }
|
.badge { @apply inline-flex items-center text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-md; }
|
||||||
.badge-ok { @apply bg-ok/10 text-ok; }
|
.badge-ok { @apply bg-ok/10 text-ok; }
|
||||||
|
|
@ -2493,10 +2516,31 @@ tailwind.config = {
|
||||||
.empty { @apply text-center py-16 text-dm dark:text-dm-dark; }
|
.empty { @apply text-center py-16 text-dm dark:text-dm-dark; }
|
||||||
.empty span { @apply text-5xl opacity-40 block mb-2; }
|
.empty span { @apply text-5xl opacity-40 block mb-2; }
|
||||||
.empty p { @apply text-sm; }
|
.empty p { @apply text-sm; }
|
||||||
.modal { @apply bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl w-full shadow-pop; }
|
.modal { @apply min-w-0 bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl w-full shadow-pop flex flex-col overflow-hidden;
|
||||||
.modal-head { @apply px-4 sm:px-5 py-4 text-base font-extrabold text-wh dark:text-wh-dark flex items-center gap-2.5 border-b border-bd dark:border-bd-dark; }
|
max-width: calc(100vw - 2rem);
|
||||||
.modal-body { @apply px-4 sm:px-5 py-4; }
|
max-height: calc(100vh - 2rem);
|
||||||
.modal-foot { @apply px-4 sm:px-5 py-3 sm:py-4 flex flex-wrap justify-end gap-2 border-t border-bd dark:border-bd-dark; }
|
max-height: calc(100dvh - 2rem); }
|
||||||
|
.modal-head { @apply min-w-0 shrink-0 px-4 sm:px-5 py-4 text-base font-extrabold text-wh dark:text-wh-dark flex items-center gap-2.5 border-b border-bd dark:border-bd-dark; }
|
||||||
|
.modal-body { @apply min-h-0 flex-1 px-4 sm:px-5 py-4 overflow-y-auto overflow-x-auto; }
|
||||||
|
.modal-foot { @apply shrink-0 px-4 sm:px-5 py-3 sm:py-4 flex flex-wrap justify-end gap-2 border-t border-bd dark:border-bd-dark; }
|
||||||
|
}
|
||||||
|
.modal-body {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.modal {
|
||||||
|
width: 100vw !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100vh !important;
|
||||||
|
max-height: 100dvh !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
||||||
|
.modal-head { flex-wrap: wrap; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
@ -2554,6 +2598,7 @@ function app() {
|
||||||
docker: { running: 0, containers: 0, images: 0 },
|
docker: { running: 0, containers: 0, images: 0 },
|
||||||
stacks: [],
|
stacks: [],
|
||||||
stackQuery: '', stackCat: '',
|
stackQuery: '', stackCat: '',
|
||||||
|
removeTarget: '', removeBusy: false,
|
||||||
storeQuery: '', storeTag: '', storeCat: '',
|
storeQuery: '', storeTag: '', storeCat: '',
|
||||||
filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false,
|
filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false,
|
||||||
hoofdmap: '', afgeleidePaden: {}, padCheck: {}, padenVoorWijziging: {},
|
hoofdmap: '', afgeleidePaden: {}, padCheck: {}, padenVoorWijziging: {},
|
||||||
|
|
@ -3098,12 +3143,28 @@ function app() {
|
||||||
if (d.job_id) this.pollJob(d.job_id, () => this.loadStacks());
|
if (d.job_id) this.pollJob(d.job_id, () => this.loadStacks());
|
||||||
else if (d.error) this.toast('Fout: '+d.error, 'er');
|
else if (d.error) this.toast('Fout: '+d.error, 'er');
|
||||||
},
|
},
|
||||||
confirmRemove(name) { this.install.stack = name; this.modal = 'remove'; },
|
confirmRemove(name) {
|
||||||
|
this.removeTarget = name;
|
||||||
|
this.removeBusy = false;
|
||||||
|
this.modal = 'remove';
|
||||||
|
},
|
||||||
async doRemove(mode) {
|
async doRemove(mode) {
|
||||||
const name = this.install.stack;
|
const name = this.removeTarget;
|
||||||
this.modal = null;
|
if (!name || this.removeBusy) return;
|
||||||
|
this.removeBusy = true;
|
||||||
const d = await this.rpc(`/api/stacks/${encodeURIComponent(name)}/remove`, { mode });
|
const d = await this.rpc(`/api/stacks/${encodeURIComponent(name)}/remove`, { mode });
|
||||||
if (d.job_id) this.pollJob(d.job_id, () => this.loadStacks());
|
if (!d.job_id) {
|
||||||
|
this.removeBusy = false;
|
||||||
|
this.toast(d.msg || d.error || this.t('remove_failed'), 'er');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.modal = null;
|
||||||
|
this.pollJob(d.job_id, async (status) => {
|
||||||
|
this.removeBusy = false;
|
||||||
|
await this.loadStacks();
|
||||||
|
this.toast(this.t(status === 'done' ? 'remove_done' : 'remove_failed'),
|
||||||
|
status === 'done' ? 'ok' : 'er');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async openCredentials(stack) {
|
async openCredentials(stack) {
|
||||||
const d = await this.rpc(`/api/stacks/${encodeURIComponent(stack)}/credentials`);
|
const d = await this.rpc(`/api/stacks/${encodeURIComponent(stack)}/credentials`);
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@
|
||||||
"instance_name": "Instance name",
|
"instance_name": "Instance name",
|
||||||
"instance_hint": "Change for multiple installations",
|
"instance_hint": "Change for multiple installations",
|
||||||
"remove_stack": "Remove app",
|
"remove_stack": "Remove app",
|
||||||
"remove_stack_only": "Stop containers + volumes",
|
"remove_stack_only": "Remove app · keep data",
|
||||||
"remove_all": "Stop + remove all files",
|
"remove_all": "Remove app and all data",
|
||||||
"prune_images": "Remove all unused images?",
|
"prune_images": "Remove all unused images?",
|
||||||
"confirm_remove_image": "Remove image?",
|
"confirm_remove_image": "Remove image?",
|
||||||
"force_remove": "Force removal?",
|
"force_remove": "Force removal?",
|
||||||
|
|
@ -471,5 +471,9 @@
|
||||||
"adopt_confirm": "Adopt '{naam}' as a Server Up app?\n\nNothing is moved or copied — the folder stays where it is. Server Up will simply manage it too.",
|
"adopt_confirm": "Adopt '{naam}' as a Server Up app?\n\nNothing is moved or copied — the folder stays where it is. Server Up will simply manage it too.",
|
||||||
"remove_container_confirm": "Remove container '{naam}'?\n\nThis removes only the container, not its data or its image.",
|
"remove_container_confirm": "Remove container '{naam}'?\n\nThis removes only the container, not its data or its image.",
|
||||||
"remove_container_project": "Note: this container belongs to compose project '{project}'. It will come back as soon as that project is started elsewhere. Adopting is probably what you want.",
|
"remove_container_project": "Note: this container belongs to compose project '{project}'. It will come back as soon as that project is started elsewhere. Adopting is probably what you want.",
|
||||||
"audit_act_adopt": "Adopted"
|
"audit_act_adopt": "Adopted",
|
||||||
|
"remove_keep_data_hint": "Stops the containers and removes the app configuration. App data and named volumes are preserved.",
|
||||||
|
"remove_all_hint": "Also removes app data and non-external named volumes. This cannot be undone.",
|
||||||
|
"remove_done": "App removed",
|
||||||
|
"remove_failed": "Failed to remove app"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@
|
||||||
"instance_name": "Instantie naam",
|
"instance_name": "Instantie naam",
|
||||||
"instance_hint": "Wijzig voor meerdere installaties",
|
"instance_hint": "Wijzig voor meerdere installaties",
|
||||||
"remove_stack": "App verwijderen",
|
"remove_stack": "App verwijderen",
|
||||||
"remove_stack_only": "Stop containers + volumes",
|
"remove_stack_only": "App verwijderen · data bewaren",
|
||||||
"remove_all": "Stop + verwijder alle bestanden",
|
"remove_all": "App en alle data verwijderen",
|
||||||
"prune_images": "Alle ongebruikte images verwijderen?",
|
"prune_images": "Alle ongebruikte images verwijderen?",
|
||||||
"confirm_remove_image": "Image verwijderen?",
|
"confirm_remove_image": "Image verwijderen?",
|
||||||
"force_remove": "Forceer verwijdering?",
|
"force_remove": "Forceer verwijdering?",
|
||||||
|
|
@ -471,5 +471,9 @@
|
||||||
"adopt_confirm": "'{naam}' overnemen als app van Server Up?\n\nEr wordt niets verplaatst of gekopieerd — de map blijft staan waar hij staat. Server Up gaat hem alleen mee beheren.",
|
"adopt_confirm": "'{naam}' overnemen als app van Server Up?\n\nEr wordt niets verplaatst of gekopieerd — de map blijft staan waar hij staat. Server Up gaat hem alleen mee beheren.",
|
||||||
"remove_container_confirm": "Container '{naam}' verwijderen?\n\nDit verwijdert alleen de container, niet zijn gegevens of zijn image.",
|
"remove_container_confirm": "Container '{naam}' verwijderen?\n\nDit verwijdert alleen de container, niet zijn gegevens of zijn image.",
|
||||||
"remove_container_project": "Let op: deze container hoort bij het compose-project '{project}'. Hij komt terug zodra dat project ergens anders wordt gestart. Overnemen is dan waarschijnlijk wat je zoekt.",
|
"remove_container_project": "Let op: deze container hoort bij het compose-project '{project}'. Hij komt terug zodra dat project ergens anders wordt gestart. Overnemen is dan waarschijnlijk wat je zoekt.",
|
||||||
"audit_act_adopt": "Overgenomen"
|
"audit_act_adopt": "Overgenomen",
|
||||||
|
"remove_keep_data_hint": "Stopt de containers en verwijdert de appconfiguratie. Appdata en named volumes blijven bewaard.",
|
||||||
|
"remove_all_hint": "Verwijdert ook appdata en niet-externe named volumes. Dit kan niet ongedaan worden gemaakt.",
|
||||||
|
"remove_done": "App verwijderd",
|
||||||
|
"remove_failed": "App verwijderen mislukt"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,42 @@ check('balk staat halverwege', c.job.progress.pct === 50);
|
||||||
// De poortcontrole praat met de backend, dus die checks zijn asynchroon.
|
// De poortcontrole praat met de backend, dus die checks zijn asynchroon.
|
||||||
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
|
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// ── App verwijderen ───────────────────────────────────────────────────────
|
||||||
|
// De veilige keuze hoort de appconfiguratie weg te halen en data te bewaren.
|
||||||
|
let verwijderVerzoek = null;
|
||||||
|
let verwijderKlaar = null;
|
||||||
|
let verwijderMelding = null;
|
||||||
|
let herladenNaVerwijderen = 0;
|
||||||
|
c.rpc = async (url, body) => {
|
||||||
|
verwijderVerzoek = {url, body};
|
||||||
|
return {job_id: 'remove-1'};
|
||||||
|
};
|
||||||
|
c.pollJob = (id, callback) => { verwijderKlaar = callback; };
|
||||||
|
c.loadStacks = async () => { herladenNaVerwijderen++; };
|
||||||
|
c.toast = (msg, kind) => { verwijderMelding = {msg, kind}; };
|
||||||
|
|
||||||
|
c.confirmRemove('mijn app');
|
||||||
|
check('verwijdervenster onthoudt de juiste app',
|
||||||
|
c.modal === 'remove' && c.removeTarget === 'mijn app');
|
||||||
|
await c.doRemove('full');
|
||||||
|
check('veilige verwijdering gebruikt full en een gecodeerde appnaam',
|
||||||
|
verwijderVerzoek.url === '/api/stacks/mijn%20app/remove' &&
|
||||||
|
verwijderVerzoek.body.mode === 'full');
|
||||||
|
check('verwijdervenster sluit zodra de taak start', c.modal === null);
|
||||||
|
await verwijderKlaar('done');
|
||||||
|
check('na verwijderen wordt de applijst opnieuw geladen',
|
||||||
|
herladenNaVerwijderen === 1 && c.removeBusy === false);
|
||||||
|
check('succesvolle verwijdering wordt gemeld',
|
||||||
|
verwijderMelding.msg === 'remove_done' && verwijderMelding.kind === 'ok');
|
||||||
|
|
||||||
|
c.confirmRemove('kapotte-app');
|
||||||
|
c.rpc = async () => ({ok: false, msg: 'kan niet verwijderen'});
|
||||||
|
await c.doRemove('all');
|
||||||
|
check('backendfout blijft zichtbaar en ontgrendelt de knop',
|
||||||
|
c.modal === 'remove' && c.removeBusy === false &&
|
||||||
|
verwijderMelding.msg === 'kan niet verwijderen' &&
|
||||||
|
verwijderMelding.kind === 'er');
|
||||||
|
|
||||||
// Een expliciet ongeschikte architectuur opent zelfs het formulier niet.
|
// Een expliciet ongeschikte architectuur opent zelfs het formulier niet.
|
||||||
let previewGevraagd = false;
|
let previewGevraagd = false;
|
||||||
let platformMelding = '';
|
let platformMelding = '';
|
||||||
|
|
|
||||||
91
tests/test_stack_remove.py
Normal file
91
tests/test_stack_remove.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""Apps verwijderen: veilige dataretentie, volledige wisactie en symlinks."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from conftest import login
|
||||||
|
|
||||||
|
|
||||||
|
def _wacht(client, jid, timeout=5):
|
||||||
|
einde = time.time() + timeout
|
||||||
|
while time.time() < einde:
|
||||||
|
data = client.get(f"/api/jobs/{jid}").get_json() or {}
|
||||||
|
if data.get("status") in ("done", "error"):
|
||||||
|
return data
|
||||||
|
time.sleep(0.03)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _stack(env, naam):
|
||||||
|
pad = env["lib"] / naam
|
||||||
|
pad.mkdir()
|
||||||
|
(pad / "compose.yaml").write_text(
|
||||||
|
"services:\n web:\n image: nginx\n", encoding="utf-8")
|
||||||
|
data = env["tmp"] / "appdata" / naam
|
||||||
|
data.mkdir(parents=True)
|
||||||
|
(data / "blijft.txt").write_text("data", encoding="utf-8")
|
||||||
|
return pad, data
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_verwijderen_haalt_app_weg_en_bewaart_data(
|
||||||
|
client, env, monkeypatch):
|
||||||
|
from core import docker
|
||||||
|
csrf = login(client)
|
||||||
|
env["core"].patch({"BACKUP_BEFORE_REMOVE": False})
|
||||||
|
stack, data = _stack(env, "veilig")
|
||||||
|
volumes = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
docker, "compose_down",
|
||||||
|
lambda *a, **kw: volumes.append(kw.get("volumes")) or 0)
|
||||||
|
|
||||||
|
r = client.post("/api/stacks/veilig/remove", json={"mode": "full"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
job = _wacht(client, r.get_json()["job_id"])
|
||||||
|
|
||||||
|
assert job["status"] == "done"
|
||||||
|
assert not stack.exists()
|
||||||
|
assert data.is_dir()
|
||||||
|
assert volumes == [False]
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_en_data_verwijderen_wist_ook_volumes_en_appdata(
|
||||||
|
client, env, monkeypatch):
|
||||||
|
from core import docker
|
||||||
|
csrf = login(client)
|
||||||
|
env["core"].patch({"BACKUP_BEFORE_REMOVE": False})
|
||||||
|
stack, data = _stack(env, "alles")
|
||||||
|
volumes = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
docker, "compose_down",
|
||||||
|
lambda *a, **kw: volumes.append(kw.get("volumes")) or 0)
|
||||||
|
|
||||||
|
r = client.post("/api/stacks/alles/remove", json={"mode": "all"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
job = _wacht(client, r.get_json()["job_id"])
|
||||||
|
|
||||||
|
assert job["status"] == "done"
|
||||||
|
assert not stack.exists()
|
||||||
|
assert not data.exists()
|
||||||
|
assert volumes == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_overgenomen_app_verwijderen_haalt_alleen_symlink_weg(
|
||||||
|
client, env, monkeypatch):
|
||||||
|
from core import docker
|
||||||
|
csrf = login(client)
|
||||||
|
env["core"].patch({"BACKUP_BEFORE_REMOVE": False})
|
||||||
|
bron = env["tmp"] / "eigen-compose"
|
||||||
|
bron.mkdir()
|
||||||
|
(bron / "compose.yaml").write_text(
|
||||||
|
"services:\n web:\n image: nginx\n", encoding="utf-8")
|
||||||
|
link = env["lib"] / "overgenomen"
|
||||||
|
link.symlink_to(bron)
|
||||||
|
monkeypatch.setattr(docker, "compose_down", lambda *a, **kw: 0)
|
||||||
|
|
||||||
|
r = client.post("/api/stacks/overgenomen/remove",
|
||||||
|
json={"mode": "full"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200, r.get_json()
|
||||||
|
job = _wacht(client, r.get_json()["job_id"])
|
||||||
|
|
||||||
|
assert job["status"] == "done"
|
||||||
|
assert not link.exists() and not link.is_symlink()
|
||||||
|
assert (bron / "compose.yaml").is_file()
|
||||||
|
|
@ -93,3 +93,12 @@ def test_de_html_van_de_pagina_is_gebalanceerd():
|
||||||
.read_text(encoding="utf-8"))
|
.read_text(encoding="utf-8"))
|
||||||
assert not p.fouten, p.fouten[:5]
|
assert not p.fouten, p.fouten[:5]
|
||||||
assert not p.stapel, [f"<{t}> op regel {r} gaat nooit dicht" for t, r in p.stapel[:5]]
|
assert not p.stapel, [f"<{t}> op regel {r} gaat nooit dicht" for t, r in p.stapel[:5]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_modals_blijven_binnen_het_mobiele_scherm_en_scrollbaar():
|
||||||
|
html = (WORTEL / "server-up" / "templates" / "index.html").read_text(
|
||||||
|
encoding="utf-8")
|
||||||
|
assert "height: 100dvh" in html
|
||||||
|
assert "max-height: 100dvh" in html
|
||||||
|
assert ".modal-body" in html and "overflow-y-auto" in html
|
||||||
|
assert "-webkit-overflow-scrolling: touch" in html
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue