From f96a9407d57cf653c6fcada5f87ceeae75b202a8 Mon Sep 17 00:00:00 2001 From: bes-r Date: Sat, 30 May 2026 00:55:16 +0200 Subject: [PATCH 1/3] fix: voorkom duplicate repo-IDs bij toevoegen via UI --- server-up/app.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/server-up/app.py b/server-up/app.py index e2a1f2e..6765cf3 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -316,7 +316,13 @@ def api_repos_add(): if not url: return jsonify(ok=False, msg="URL vereist") repos = list(cfg.load().get("APP_REPOS", [])) - rid = d.get("id", url.split("/")[-1].replace(".git", "")).strip()[:30] or str(int(time.time())) + existing_ids = {r["id"] for r in repos} + base_rid = d.get("id", url.split("/")[-1].replace(".git", "")).strip()[:30] or str(int(time.time())) + rid = base_rid + counter = 1 + while rid in existing_ids: + rid = f"{base_rid}-{counter}" + counter += 1 repos.append({"id": rid, "name": d.get("name", "").strip() or url.split("/")[-1], "url": url, "branch": d.get("branch", "main"), "token": d.get("token", ""), "subdir": d.get("subdir", "")}) @@ -877,8 +883,14 @@ def api_modrepos_add(): if not url: return jsonify(ok=False, msg="URL vereist") repos = list(cfg.load().get("MODULE_REPOS", [])) - repos.append({"id": d.get("id", url.split("/")[-1].replace(".git", ""))[:30], - "name": d.get("name", "") or url.split("/")[-1], + existing_ids = {r["id"] for r in repos} + base_rid = d.get("id", url.split("/")[-1].replace(".git", ""))[:30] + rid = base_rid + counter = 1 + while rid in existing_ids: + rid = f"{base_rid}-{counter}" + counter += 1 + repos.append({"id": rid, "name": d.get("name", "") or url.split("/")[-1], "url": url, "branch": d.get("branch", "main"), "subdir": d.get("subdir", "")}) cfg.patch({"MODULE_REPOS": repos}) From d866a13c200629f8fe99a721918f803ad0656b2f Mon Sep 17 00:00:00 2001 From: bes-r Date: Tue, 2 Jun 2026 23:07:27 +0200 Subject: [PATCH 2/3] UI: grotere/mobielvriendelijke restyle, inklapbaar menu, taalmodule add-on --- server-up/app.py | 35 ++++++ server-up/core/i18n.py | 77 +++++++++++- server-up/templates/index.html | 214 ++++++++++++++++++++++++++------- server-up/translations/en.json | 18 ++- server-up/translations/nl.json | 18 ++- 5 files changed, 316 insertions(+), 46 deletions(-) diff --git a/server-up/app.py b/server-up/app.py index 6765cf3..e57815d 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -1012,6 +1012,41 @@ def api_i18n_lang(lang): return jsonify(strings=i18n.strings(lang)) +@app.route("/api/i18n/keys") +def api_i18n_keys(): + """Referentie: alle vertaalsleutels + Engelse waardes (voor de editor).""" + return jsonify(keys=i18n.reference_keys(), en=i18n.strings("en")) + + +@app.route("/api/i18n//raw") +def api_i18n_lang_raw(lang): + """Alleen de eigen strings van deze taal (voor bewerken).""" + return jsonify(strings=i18n.raw(lang), builtin=i18n.is_builtin(lang)) + + +@app.route("/api/i18n", methods=["POST"]) +def api_i18n_save(): + """Voeg een taal toe of werk er een bij (add-on).""" + body = request.get_json(silent=True) or {} + res = i18n.save( + code=body.get("code", ""), + name=body.get("name", ""), + flag=body.get("flag", ""), + entries=body.get("strings", {}), + ) + if res.get("ok"): + audit.log("i18n", "save", "ok", ref=res.get("code")) + return jsonify(res), (200 if res.get("ok") else 400) + + +@app.route("/api/i18n/", methods=["DELETE"]) +def api_i18n_delete(lang): + res = i18n.delete(lang) + if res.get("ok"): + audit.log("i18n", "delete", "ok", ref=lang) + return jsonify(res), (200 if res.get("ok") else 400) + + # ── Wizard ──────────────────────────────────────────────────────────────────── @app.route("/api/wizard") diff --git a/server-up/core/i18n.py b/server-up/core/i18n.py index e02881d..0f6aca6 100644 --- a/server-up/core/i18n.py +++ b/server-up/core/i18n.py @@ -1,11 +1,16 @@ -"""Internationalisation — built-in NL + EN.""" +"""Internationalisation — built-in NL + EN, plus add-on talen via UI.""" import json +import re from pathlib import Path _DIR = Path(__file__).parent.parent / "translations" _cache: dict[str, dict] = {} _langs: dict[str, dict] = {} +# Ingebouwde talen worden meegeleverd en kunnen niet verwijderd worden. +BUILTIN = frozenset({"nl", "en"}) +_CODE_RE = re.compile(r"^[a-z]{2,3}(-[a-z0-9]{2,8})?$", re.IGNORECASE) + def load(): _cache.clear() @@ -26,14 +31,82 @@ def load(): def available() -> list[dict]: if not _langs: load() - return [{"code": k, "name": v.get("name", k), "flag": v.get("flag", "")} + return [{"code": k, + "name": v.get("name", k), + "flag": v.get("flag", ""), + "builtin": k in BUILTIN, + "count": len(_cache.get(k, {}))} for k, v in _langs.items()] def strings(lang: str) -> dict: + """Samengevoegde strings: Engelse basis + taal-overrides (voor weergave).""" if not _cache: load() base = dict(_cache.get("en", {})) if lang != "en" and lang in _cache: base.update(_cache[lang]) return base + + +def raw(lang: str) -> dict: + """Alleen de eigen strings van deze taal (voor bewerken).""" + if not _cache: + load() + return dict(_cache.get(lang, {})) + + +def reference_keys() -> list[str]: + """Alle vertaalsleutels (op basis van de Engelse bron) als referentie.""" + if not _cache: + load() + return sorted(_cache.get("en", {}).keys()) + + +def is_builtin(code: str) -> bool: + return code in BUILTIN + + +def _path_for(code: str) -> Path: + return _DIR / f"{code}.json" + + +def save(code: str, name: str, flag: str, entries: dict) -> dict: + """Sla een (nieuwe of bestaande) taal op als add-on. Retourneert {ok, msg}.""" + code = (code or "").strip().lower() + if not _CODE_RE.match(code): + return {"ok": False, "msg": "Ongeldige taalcode (bv. 'de', 'fr', 'pt-br')."} + if code in BUILTIN: + return {"ok": False, "msg": f"'{code}' is een ingebouwde taal en kan niet overschreven worden."} + if not isinstance(entries, dict): + return {"ok": False, "msg": "Strings moeten een object zijn."} + + _DIR.mkdir(parents=True, exist_ok=True) + clean = {k: str(v) for k, v in entries.items() + if not str(k).startswith("_") and v not in (None, "")} + doc = {"_meta": {"code": code, + "name": (name or code).strip(), + "flag": (flag or "").strip()}} + doc.update(clean) + try: + _path_for(code).write_text( + json.dumps(doc, ensure_ascii=False, indent=2), "utf-8") + except Exception as e: + return {"ok": False, "msg": f"Schrijven mislukt: {e}"} + load() + return {"ok": True, "msg": f"Taal '{name or code}' opgeslagen.", "code": code} + + +def delete(code: str) -> dict: + code = (code or "").strip().lower() + if code in BUILTIN: + return {"ok": False, "msg": "Ingebouwde talen kunnen niet verwijderd worden."} + p = _path_for(code) + if not p.exists(): + return {"ok": False, "msg": "Taal niet gevonden."} + try: + p.unlink() + except Exception as e: + return {"ok": False, "msg": f"Verwijderen mislukt: {e}"} + load() + return {"ok": True, "msg": "Taal verwijderd."} diff --git a/server-up/templates/index.html b/server-up/templates/index.html index 7734e08..6831ef2 100644 --- a/server-up/templates/index.html +++ b/server-up/templates/index.html @@ -56,54 +56,54 @@ tailwind.config = { .log-section { color:#22d3ee; font-weight:600 } - + -
- - ServerUp - +
+ + ServerUp +
-
-