v0.8.80-beta - Poortcontrole in het formulier, en installeren op een telefoon
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 15m55s

- /api/ports/check bestond al en kijkt naar wat Docker publiceert en naar wat
  er rechtstreeks op de host luistert, maar het installatieformulier riep hem
  nergens aan. Er stond hooguit een regeltje als de standaardwaarde bezet was;
  typte je zelf iets, dan hoorde je niets
- Een poortveld heeft nu een min- en plusknop, een vergrootglas om te
  controleren, en een melding: vrij, of bezet en door welke container, met een
  knop om naar de eerstvolgende vrije poort te springen. De controle loopt ook
  bij het openen van een stap
- Kan de host niet uitgelezen worden, dan zegt de melding dat "vrij" een
  aanname is in plaats van dat te verzwijgen
- De knoppen zitten alleen op velden die ook echt een poort zijn; puid en pgid
  zijn ook getallen
- De installatiemodal is op een telefoon schermvullend, met de kop en de
  knoppenbalk vast en alleen de inhoud die scrollt. Vorige en Volgende vullen
  samen de breedte
- Sluitknop in de kop van de modal: schermvullend betekent geen achtergrond meer
  om op te tikken, en die was er nog niet

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
Ramon 2026-08-02 07:57:05 +02:00
parent 0c025d657d
commit ef945fcd71
6 changed files with 223 additions and 23 deletions

View file

@ -1,3 +1,29 @@
# v0.8.80-beta — Poorten kiezen zonder gokken, en installeren op een telefoon
**De poortcontrole zat er wel, maar werd nooit gevraagd.** `/api/ports/check`
bestaat en kijkt naar wat Docker publiceert én naar wat er rechtstreeks op de
host luistert — dat laatste is precies de botsing die je anders pas ziet als de
stack niet start. Het installatieformulier riep hem alleen nergens aan. Er stond
hooguit een regeltje als de stándaardwaarde bezet was; typte je zelf iets, dan
hoorde je niets meer.
Een poortveld heeft nu knoppen om een stapje omhoog of omlaag te doen, een
vergrootglas om te controleren, en een melding eronder: vrij, of bezet en door
welke container, met een knop om meteen naar de eerstvolgende vrije poort te
springen. De controle loopt ook zodra je een stap opent, dus je ziet het vóórdat
je iets typt. Kan de host niet uitgelezen worden, dan zegt de melding dat "vrij"
een aanname is in plaats van het te verzwijgen.
De knoppen zitten alleen op velden die ook echt een poort zijn — `puid` en `pgid`
zijn ook getallen.
**Installeren op een telefoon.** De installatiemodal is daar nu schermvullend in
plaats van een venstertje met marges, met de kop en de knoppenbalk vast en alleen
de inhoud die scrollt. Vorige en Volgende vullen samen de breedte in plaats van
op een rare plek af te breken. En omdat er dan geen achtergrond meer is om op te
tikken, zit er een sluitknop in de kop — die was er niet, en zonder deze
wijziging was je er niet uit gekomen.
# v0.8.70-beta — De store blijft soepel, en achtentwintig apps erbij # v0.8.70-beta — De store blijft soepel, en achtentwintig apps erbij
**Typen in het zoekveld werd traag.** `filteredStore()` is een methode, geen **Typen in het zoekveld werd traag.** `filteredStore()` is een methode, geen

View file

@ -1 +1 @@
0.8.70-beta 0.8.80-beta

View file

@ -1351,7 +1351,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-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">
<!-- 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">
@ -1607,11 +1607,18 @@ tailwind.config = {
</div> </div>
<!-- Install --> <!-- Install -->
<div x-show="modal==='install'" class="modal max-w-2xl flex flex-col max-h-[88vh]"> <div x-show="modal==='install'"
class="modal max-w-2xl flex flex-col h-full max-h-full rounded-none
sm:h-auto sm:max-h-[88vh] sm:rounded-2xl">
<div class="modal-head"> <div class="modal-head">
<span class="mdi mdi-download-outline text-ac dark:text-ac-dark"></span> <span class="mdi mdi-download-outline text-ac dark:text-ac-dark shrink-0"></span>
<span x-text="install.mode === 'edit' ? t('stack_settings') : t('install_stack')">Stack installeren</span> <span class="min-w-0 truncate" x-text="install.mode === 'edit' ? t('stack_settings') : t('install_stack')">Stack installeren</span>
<span x-show="install.format === 'boilerplate'" class="badge badge-info">Boilerplate</span> <span x-show="install.format === 'boilerplate'" class="badge badge-info hidden sm:inline-flex">Boilerplate</span>
<!-- Op een telefoon vult deze modal het scherm, dus er is geen
achtergrond meer om op te tikken. Zonder deze knop kom je er niet uit. -->
<button class="btn-icon ml-auto shrink-0" @click="modal=null" :title="t('close')">
<span class="mdi mdi-close"></span>
</button>
</div> </div>
<div class="modal-body overflow-y-auto space-y-3"> <div class="modal-body overflow-y-auto space-y-3">
<!-- Bij wijzigen wordt de stack opnieuw gerenderd; dat overschrijft <!-- Bij wijzigen wordt de stack opnieuw gerenderd; dat overschrijft
@ -1934,7 +1941,49 @@ tailwind.config = {
<template x-if="!f.connect && f.type === 'bool'"> <template x-if="!f.connect && f.type === 'bool'">
<label class="inline-flex items-center gap-2 cursor-pointer"><input type="checkbox" :checked="!!install.values[f.name]" @change="install.values[f.name] = $event.target.checked" class="rounded border-bd dark:border-bd-dark text-ac w-5 h-5"><span class="text-xs text-t2 dark:text-t2-dark" x-text="f.description"></span></label> <label class="inline-flex items-center gap-2 cursor-pointer"><input type="checkbox" :checked="!!install.values[f.name]" @change="install.values[f.name] = $event.target.checked" class="rounded border-bd dark:border-bd-dark text-ac w-5 h-5"><span class="text-xs text-t2 dark:text-t2-dark" x-text="f.description"></span></label>
</template> </template>
<template x-if="!f.connect && f.type === 'int'"><input type="number" class="form-input" x-model.number="install.values[f.name]" :placeholder="f.placeholder"></template> <!-- Poortveld: knoppen om te stappen, en een
controle of hij vrij is. De backend kon dat al
(/api/ports/check, kijkt naar Docker én naar wat
er rechtstreeks op de host luistert), maar het
formulier vroeg het nooit. -->
<template x-if="!f.connect && f.type === 'int' && isPoortVeld(f)">
<div>
<div class="flex">
<button type="button" class="stepper rounded-l-xl border-r-0"
@click="stapPoort(f, -1)" :title="t('lower')">
<span class="mdi mdi-minus"></span>
</button>
<input type="number" inputmode="numeric"
class="form-input !rounded-none text-center"
x-model.number="install.values[f.name]"
@input.debounce.400ms="checkPoort(f)"
:placeholder="f.placeholder">
<button type="button" class="stepper border-l-0"
@click="stapPoort(f, 1)" :title="t('raise')">
<span class="mdi mdi-plus"></span>
</button>
<button type="button" class="stepper rounded-r-xl border-l-0"
@click="checkPoort(f)" :title="t('check_availability')">
<span class="mdi" :class="poortCheck[f.name] && poortCheck[f.name].bezig ? 'mdi-loading mdi-spin' : 'mdi-magnify'"></span>
</button>
</div>
<template x-if="poortCheck[f.name] && !poortCheck[f.name].bezig">
<p class="text-[11px] mt-1 flex items-start gap-1.5"
:class="poortCheck[f.name].free ? 'text-ok' : 'text-wr'">
<span class="mdi shrink-0"
:class="poortCheck[f.name].free ? 'mdi-check-circle-outline' : 'mdi-alert-circle-outline'"></span>
<span class="min-w-0">
<span x-text="poortStatus(f)"></span>
<button type="button" x-show="!poortCheck[f.name].free"
class="underline ml-1"
@click="install.values[f.name] = poortCheck[f.name].suggested; checkPoort(f)"
x-text="t('use_free_port', {port: poortCheck[f.name].suggested})"></button>
</span>
</p>
</template>
</div>
</template>
<template x-if="!f.connect && f.type === 'int' && !isPoortVeld(f)"><input type="number" class="form-input" x-model.number="install.values[f.name]" :placeholder="f.placeholder"></template>
<template x-if="!f.connect && f.type === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template> <template x-if="!f.connect && f.type === 'str' && !f.secret"><input class="form-input" x-model="install.values[f.name]" :placeholder="f.placeholder"></template>
<!-- Geheim veld: verborgen tekst, oog om te tonen, dobbelsteen waar een <!-- Geheim veld: verborgen tekst, oog om te tonen, dobbelsteen waar een
willekeurige waarde klopt (niet bij een sleutel die ergens bij moet passen) --> willekeurige waarde klopt (niet bij een sleutel die ergens bij moet passen) -->
@ -1950,8 +1999,6 @@ tailwind.config = {
@click="install.values[f.name] = veldSecret(f); revealed[f.name] = true"><span class="mdi mdi-dice-5-outline"></span></button> @click="install.values[f.name] = veldSecret(f); revealed[f.name] = true"><span class="mdi mdi-dice-5-outline"></span></button>
</div> </div>
</template> </template>
<p x-show="f.port_taken" class="text-[11px] text-wr mt-1"
x-text="t('port_taken', {port: f.port_taken, suggested: f.suggested})"></p>
<p x-show="f.description && f.type !== 'bool'" class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="f.description"></p> <p x-show="f.description && f.type !== 'bool'" class="text-[11px] text-dm dark:text-dm-dark mt-1" x-text="f.description"></p>
</div> </div>
</template> </template>
@ -2032,15 +2079,15 @@ tailwind.config = {
</template> </template>
</div> </div>
<div class="modal-foot"> <div class="modal-foot">
<button class="btn" @click="modal=null" x-text="t('cancel')">Annuleren</button> <button class="btn hidden sm:inline-flex" @click="modal=null" x-text="t('cancel')">Annuleren</button>
<button class="btn" x-show="wizardActief() && install.step > 0" @click="prevStep()"> <button class="btn flex-1 sm:flex-none" x-show="wizardActief() && install.step > 0" @click="prevStep()">
<span class="mdi mdi-chevron-left"></span> <span x-text="t('previous')">Vorige</span> <span class="mdi mdi-chevron-left"></span> <span x-text="t('previous')">Vorige</span>
</button> </button>
<button class="btn btn-primary" x-show="wizardActief() && !opLaatsteStap()" <button class="btn btn-primary flex-1 sm:flex-none" x-show="wizardActief() && !opLaatsteStap()"
@click="nextStep()" :disabled="stepMissing().length > 0"> @click="nextStep()" :disabled="stepMissing().length > 0">
<span x-text="t('next')">Volgende</span> <span class="mdi mdi-chevron-right"></span> <span x-text="t('next')">Volgende</span> <span class="mdi mdi-chevron-right"></span>
</button> </button>
<button class="btn btn-primary" x-show="!wizardActief() || opLaatsteStap()" <button class="btn btn-primary flex-1 sm:flex-none" x-show="!wizardActief() || opLaatsteStap()"
@click="install.mode === 'edit' ? doReconfigure() : doInstall()"> @click="install.mode === 'edit' ? doReconfigure() : doInstall()">
<span class="mdi" :class="install.mode === 'edit' ? 'mdi-content-save-outline' : 'mdi-rocket-launch'"></span> <span class="mdi" :class="install.mode === 'edit' ? 'mdi-content-save-outline' : 'mdi-rocket-launch'"></span>
<span x-text="install.mode === 'edit' ? t('save') : t('install')">Installeren</span> <span x-text="install.mode === 'edit' ? t('save') : t('install')">Installeren</span>
@ -2311,6 +2358,7 @@ tailwind.config = {
.btn-primary{ @apply !bg-ac !text-white !border-ac shadow-soft2 hover:!bg-ac2 hover:!border-ac2; } .btn-primary{ @apply !bg-ac !text-white !border-ac shadow-soft2 hover:!bg-ac2 hover:!border-ac2; }
.btn-danger { @apply !text-er !border-er/30 !bg-er/5 hover:!bg-er/10 hover:!border-er/40; } .btn-danger { @apply !text-er !border-er/30 !bg-er/5 hover:!bg-er/10 hover:!border-er/40; }
.btn-icon { @apply w-10 h-10 grid place-items-center rounded-xl border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-t2 dark:text-t2-dark hover:text-ac dark:hover:text-ac-dark hover:border-bd2 dark:hover:border-bd2-dark active:scale-95 transition cursor-pointer shrink-0; } .btn-icon { @apply w-10 h-10 grid place-items-center rounded-xl border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-t2 dark:text-t2-dark hover:text-ac dark:hover:text-ac-dark hover:border-bd2 dark:hover:border-bd2-dark active:scale-95 transition cursor-pointer shrink-0; }
.stepper { @apply w-10 h-11 grid place-items-center shrink-0 border border-bd dark:border-bd-dark bg-s2 dark:bg-s2-dark text-t2 dark:text-t2-dark hover:text-ac dark:hover:text-ac-dark hover:bg-s3 dark:hover:bg-s3-dark active:scale-95 transition cursor-pointer; }
.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; }
@ -2336,9 +2384,9 @@ tailwind.config = {
.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 bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2xl w-full shadow-pop; }
.modal-head { @apply 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-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; }
.modal-body { @apply px-5 py-4; } .modal-body { @apply px-4 sm:px-5 py-4; }
.modal-foot { @apply px-5 py-4 flex justify-end gap-2 border-t border-bd dark:border-bd-dark; } .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; }
} }
</style> </style>
@ -2398,6 +2446,7 @@ function app() {
stackQuery: '', stackCat: '', stackQuery: '', stackCat: '',
storeQuery: '', storeTag: '', storeCat: '', storeQuery: '', storeTag: '', storeCat: '',
filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false, filterOpen: false, filterTagQuery: '', stackMenu: '', stackFilterOpen: false,
poortCheck: {},
// Niet reactief nodig: puur een geheugensteun voor filteredStore(). // Niet reactief nodig: puur een geheugensteun voor filteredStore().
_storeCache: { sleutel: null, lijsten: {} }, _storeCache: { sleutel: null, lijsten: {} },
storeLimiet: 60, storeLimiet: 60,
@ -3198,6 +3247,55 @@ function app() {
this.install.connect_apps = d.connect_default !== false; this.install.connect_apps = d.connect_default !== false;
} }
}, },
// ── Poorten ──────────────────────────────────────────────────────────
// Alleen een int-veld dat ook echt een poort ís krijgt de stapknoppen en de
// controle; puid en pgid zijn ook getallen.
isPoortVeld(f) {
return /port|poort/i.test(f.name || '');
},
stapPoort(f, richting) {
const nu = parseInt(this.install.values[f.name], 10);
const nieuw = Math.min(65535, Math.max(1, (isNaN(nu) ? 8080 : nu) + richting));
this.install.values[f.name] = nieuw;
this.checkPoort(f);
},
// Is deze poort vrij? De backend kijkt naar wat Docker publiceert én naar
// wat er rechtstreeks op de host luistert — dat laatste is precies de
// botsing die je anders pas ziet als de stack niet start.
async checkPoort(f) {
const poort = parseInt(this.install.values[f.name], 10);
if (!poort || poort < 1 || poort > 65535) {
delete this.poortCheck[f.name];
return;
}
this.poortCheck[f.name] = { bezig: true };
const d = await this.rpc(`/api/ports/check?port=${poort}`);
if (!d || !d.ok) { delete this.poortCheck[f.name]; return; }
// Alleen bewaren als het antwoord nog bij de huidige waarde hoort; typen
// gaat sneller dan het netwerk.
if (parseInt(this.install.values[f.name], 10) === d.port) {
this.poortCheck[f.name] = { ...d, bezig: false };
}
},
poortStatus(f) {
const c = this.poortCheck[f.name] || {};
if (!c.free) {
return c.used_by
? this.t('port_used_by', { port: c.port, container: c.used_by })
: this.t('port_in_use', { port: c.port });
}
// Zonder de hulpcontainer zien we alleen Docker; dan is "vrij" een
// aanname en dat hoort de melding te zeggen.
return c.host_checked ? this.t('port_free') : this.t('port_free_docker_only');
},
// Bij het openen van een stap meteen controleren wat er in de poortvelden
// staat, in plaats van te wachten tot iemand iets typt.
checkPoortenVanStap(stapId) {
for (const f of this.stepFields(stapId)) {
if (f.type === 'int' && this.isPoortVeld(f)) this.checkPoort(f);
}
},
// Wat dit sjabloon nodig heeft en nog niet draait. De backend heeft de // Wat dit sjabloon nodig heeft en nog niet draait. De backend heeft de
// status er al bij gezet; hier blijft alleen het filteren over. // status er al bij gezet; hier blijft alleen het filteren over.
ontbrekendeAfhankelijkheden() { ontbrekendeAfhankelijkheden() {
@ -3390,6 +3488,9 @@ function app() {
this.install.depends_on = d.depends_on || []; this.install.depends_on = d.depends_on || [];
this.install.step = 0; this.install.step = 0;
this.revealed = {}; this.revealed = {};
this.poortCheck = {};
this.$nextTick(() => this.checkPoortenVanStap(
(this.wizardSteps()[0] || {}).id || 'basis'));
this.ipCheck = {}; this.ipCheck = {};
await this.loadAppNets(this.install.instance); await this.loadAppNets(this.install.instance);
// Standaard alleen het gedeelde netwerk aanvinken, niet alle. // Standaard alleen het gedeelde netwerk aanvinken, niet alle.
@ -3513,10 +3614,12 @@ function app() {
const stappen = this.wizardSteps(); const stappen = this.wizardSteps();
this.install.step = Math.max(0, Math.min(i, stappen.length - 1)); this.install.step = Math.max(0, Math.min(i, stappen.length - 1));
this.fieldInfo = null; this.fieldInfo = null;
const stapId = stappen[this.install.step].id;
// Ook bij Verbinden: daar staat de lijst containers die uit de // Ook bij Verbinden: daar staat de lijst containers die uit de
// voorvertoning komt, en die verandert met elk onderdeel dat je aanzet. // voorvertoning komt, en die verandert met elk onderdeel dat je aanzet.
if (['controleren', 'verbinden'].includes(stappen[this.install.step].id)) if (['controleren', 'verbinden'].includes(stapId)) await this.refreshPreview();
await this.refreshPreview(); // Meteen laten zien of de poorten van deze stap vrij zijn.
this.checkPoortenVanStap(stapId);
}, },
async nextStep() { async nextStep() {
if (this.stepMissing().length) return; if (this.stepMissing().length) return;

View file

@ -450,5 +450,10 @@
"audit_act_install_pangolin": "Pangolin installed", "audit_act_install_pangolin": "Pangolin installed",
"backup_appdata": "Include app data", "backup_appdata": "Include app data",
"backup_appdata_hint": "The app's own data, plus a dump of every database. Turn this off for a media library or photo archive — there it runs into hundreds of gigabytes and restic or borg is the better tool.", "backup_appdata_hint": "The app's own data, plus a dump of every database. Turn this off for a media library or photo archive — there it runs into hundreds of gigabytes and restic or borg is the better tool.",
"show_more": "Show {n} more" "show_more": "Show {n} more",
"lower": "Decrease",
"raise": "Increase",
"port_free_docker_only": "Free according to Docker — what listens directly on the host could not be checked",
"port_used_by": "Port {port} is in use by {container}",
"use_free_port": "use {port}"
} }

View file

@ -450,5 +450,10 @@
"audit_act_install_pangolin": "Pangolin geïnstalleerd", "audit_act_install_pangolin": "Pangolin geïnstalleerd",
"backup_appdata": "Appdata meenemen", "backup_appdata": "Appdata meenemen",
"backup_appdata_hint": "De gegevens van de app zelf, plus een dump van elke database. Zet dit uit bij een mediabibliotheek of fotoarchief — daar loopt het in de honderden gigabytes en is restic of borg een beter middel.", "backup_appdata_hint": "De gegevens van de app zelf, plus een dump van elke database. Zet dit uit bij een mediabibliotheek of fotoarchief — daar loopt het in de honderden gigabytes en is restic of borg een beter middel.",
"show_more": "Nog {n} tonen" "show_more": "Nog {n} tonen",
"lower": "Omlaag",
"raise": "Omhoog",
"port_free_docker_only": "Vrij volgens Docker — wat rechtstreeks op de host luistert kon niet gecontroleerd worden",
"port_used_by": "Poort {port} is in gebruik door {container}",
"use_free_port": "gebruik {port}"
} }

View file

@ -17,7 +17,10 @@ global.navigator = { clipboard: { writeText: async () => {} }, language: 'nl' };
global.crypto = require('crypto').webcrypto; global.crypto = require('crypto').webcrypto;
eval(bron); eval(bron);
const c = app(); const c = app();
c.t = (k, v) => k; // vertaling niet nodig voor de logica // De echte t() vult {plaatshouders} in. Dat nabootsen scheelt niet alleen
// realisme: zonder invulling kun je niet toetsen of de containernaam in de
// melding belandt.
c.t = (k, v) => v ? k + ':' + Object.values(v).join(',') : k;
c.bpFieldVisible = () => true; c.bpFieldVisible = () => true;
c.rpc = async () => ({ok: true, preview_files: {}, env_preview: ''}); c.rpc = async () => ({ok: true, preview_files: {}, env_preview: ''});
c.toast = () => {}; c.toast = () => {};
@ -92,6 +95,35 @@ check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
c.install.fields.push(veld('c','basis',{group:'Tweede'})); c.install.fields.push(veld('c','basis',{group:'Tweede'}));
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader)); check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
// ── Poortveld ───────────────────────────────────────────────────────────────
// De backend kon al controleren of een poort vrij is, maar het formulier vroeg
// het nooit. En je kon alleen typen — geen manier om er snel eentje bij te doen.
check('een poortveld herkennen', c.isPoortVeld({name: 'port'})
&& c.isPoortVeld({name: 'port_ui'}) && c.isPoortVeld({name: 'poort'}));
check('puid is geen poort', !c.isPoortVeld({name: 'puid'})
&& !c.isPoortVeld({name: 'pgid'}));
c.install = {format:'boilerplate', loading:false, fields:[], values:{port: 8080}, step:0};
c.poortCheck = {};
let gevraagd = [];
c.rpc = async (u) => {
gevraagd.push(u);
const p = Number(new URL('http://x' + u).searchParams.get('port'));
return {ok:true, port:p, free:p !== 8080, suggested:8081,
used_by:p === 8080 ? 'nginx' : '', host_checked:true};
};
c.stapPoort({name:'port'}, 1);
check('plus verhoogt', c.install.values.port === 8081);
c.stapPoort({name:'port'}, -1);
check('min verlaagt', c.install.values.port === 8080);
c.install.values.port = 1;
c.stapPoort({name:'port'}, -1);
check('niet onder 1', c.install.values.port === 1);
c.install.values.port = 65535;
c.stapPoort({name:'port'}, 1);
check('niet boven 65535', c.install.values.port === 65535);
// ── Leesbare namen ────────────────────────────────────────────────────────── // ── Leesbare namen ──────────────────────────────────────────────────────────
// Het auditlog bewaart ruwe id's; die horen niet in beeld te staan. // 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'}; c.T = {audit_cat_store: 'App Store', audit_act_reconfigure: 'Opnieuw ingesteld'};
@ -287,5 +319,34 @@ c.install.ips['arr-sonarr'] = '192.168.1.240';
c.zetContainerIp('arr-sonarr', false); c.zetContainerIp('arr-sonarr', false);
check('en weer uitgezet', !('arr-sonarr' in c.install.ips)); check('en weer uitgezet', !('arr-sonarr' in c.install.ips));
// De poortcontrole praat met de backend, dus die checks zijn asynchroon.
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
(async () => {
// Een leeg of onzinnig veld hoort geen verzoek te sturen.
c.install.values.port = 0;
gevraagd = [];
await c.checkPoort({name:'port'});
check('geen controle bij een lege poort', gevraagd.length === 0);
check('en geen status', !c.poortCheck.port);
c.install.values.port = 8080;
await c.checkPoort({name:'port'});
check('bezette poort wordt gemeld', c.poortCheck.port.free === false);
check('met de container erbij',
c.poortStatus({name:'port'}).includes('nginx'));
check('en een voorstel', c.poortCheck.port.suggested === 8081);
c.install.values.port = 8081;
await c.checkPoort({name:'port'});
check('vrije poort wordt gemeld', c.poortCheck.port.free === true);
// Zonder de hulpcontainer is "vrij" een aanname; dat hoort de melding te zeggen.
c.rpc = async () => ({ok:true, port:8081, free:true, suggested:8081,
used_by:'', host_checked:false});
await c.checkPoort({name:'port'});
check('onzekerheid wordt benoemd',
c.poortStatus({name:'port'}) !== c.t('port_free'));
console.log(fouten === 0 ? '\nAlle menucontroles geslaagd' : `\n${fouten} FOUTEN`); console.log(fouten === 0 ? '\nAlle menucontroles geslaagd' : `\n${fouten} FOUTEN`);
process.exit(fouten ? 1 : 0); process.exit(fouten ? 1 : 0);
})();