diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f65fd9..7ab7eb4 100644
--- a/CHANGELOG.md
+++ b/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
diff --git a/VERSION b/VERSION
index ca5543f..1036deb 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.8.40-beta
+0.8.50-beta
diff --git a/server-up/core/git.py b/server-up/core/git.py
index 7119138..0dd88c2 100644
--- a/server-up/core/git.py
+++ b/server-up/core/git.py
@@ -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),
diff --git a/server-up/templates/index.html b/server-up/templates/index.html
index f8ad429..74b55cd 100644
--- a/server-up/templates/index.html
+++ b/server-up/templates/index.html
@@ -173,7 +173,7 @@ tailwind.config = {
-
Dashboard
+ Dashboard
@@ -250,29 +250,52 @@ tailwind.config = {
-
Vernieuwen
+
+
+
+
+ Filter
+
+
+
+
+
+
+ Alle
+
+
+
+
+
+
+
+
+
+
+
+
+
- Updates controleren
+ Updates controleren
-
-
- Alle
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
@@ -573,6 +624,24 @@ tailwind.config = {
BP
+
+
+
+
+
+ Geïnstalleerd
+
+
+
+
+
+
+
+
+
-
- Installeren
+
+
+ Installeren
-
-
-
@@ -642,11 +710,11 @@ tailwind.config = {
Alle
-
+
Alle
-
+
Alle
@@ -670,16 +738,19 @@ tailwind.config = {
- Tijd Categorie
- Actie Status Ref
+ Tijd
+ Categorie
+ Actie
+ Status
+ Betreft
-
-
-
+
+
+
@@ -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:' — 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 = '';
diff --git a/server-up/translations/en.json b/server-up/translations/en.json
index 2f22234..790dfdf 100644
--- a/server-up/translations/en.json
+++ b/server-up/translations/en.json
@@ -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"
}
diff --git a/server-up/translations/nl.json b/server-up/translations/nl.json
index 1edfa6c..2e7ea4c 100644
--- a/server-up/translations/nl.json
+++ b/server-up/translations/nl.json
@@ -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"
}
diff --git a/tests/js/menu.test.js b/tests/js/menu.test.js
index ca39a91..d01ff9c 100644
--- a/tests/js/menu.test.js
+++ b/tests/js/menu.test.js
@@ -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.
diff --git a/tests/test_connect.py b/tests/test_connect.py
index 7a0d302..7fb5613 100644
--- a/tests/test_connect.py
+++ b/tests/test_connect.py
@@ -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"] == []
diff --git a/tests/test_i18n.py b/tests/test_i18n.py
index e6194fb..d096e94 100644
--- a/tests/test_i18n.py
+++ b/tests/test_i18n.py
@@ -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_` en `audit_act_`. 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}"