feat(repos): branch van een app-repo tonen en wijzigen
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 6m45s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 6m45s
Sjablonen komen uit de git-cache van een app-repo, niet uit het image. Welke branch die volgt was nergens te zien en alleen te wijzigen door de repo te verwijderen en opnieuw toe te voegen — waarbij de id een volgnummer krijgt en het bronsjabloon van bestaande apps onvindbaar wordt. - PUT /api/repos/<id> past naam, branch, submap en optioneel de URL aan; de id blijft staan zodat .serverup.json van bestaande stacks blijft kloppen. - Een leeg tokenveld laat het bestaande token staan. - De repolijst toont de branch en heeft een bewerkknop; na opslaan wordt de repo meteen opnieuw opgehaald. - Nieuwe auditactie 'edit' vertaald in nl en en. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
This commit is contained in:
parent
dce4e34894
commit
483f80e032
7 changed files with 187 additions and 1 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -1,3 +1,19 @@
|
||||||
|
# v0.8.23-beta — De branch van een repo is te zien en te wijzigen
|
||||||
|
|
||||||
|
Sjablonen komen uit de git-cache van een app-repo, niet uit het image van
|
||||||
|
Server Up. Welke branch die repo volgt was nergens te zien, en alleen te
|
||||||
|
veranderen door de repo te verwijderen en opnieuw toe te voegen. De id wordt
|
||||||
|
dan uit de URL afgeleid en krijgt er een volgnummer bij zodra hij al bestaat —
|
||||||
|
en juist die id staat in de metadata van elke app uit die repo. Overstappen
|
||||||
|
maakte zo stilzwijgend het bronsjabloon van je apps onvindbaar.
|
||||||
|
|
||||||
|
- **De branch staat in de lijst.** Zo is te zien waarom een app een oudere
|
||||||
|
versie van zijn sjabloon blijft renderen.
|
||||||
|
- **Bewerken zonder de id te raken.** Naam, branch en submap zijn aan te passen;
|
||||||
|
na opslaan wordt de repo meteen opnieuw opgehaald.
|
||||||
|
- **Een leeg tokenveld wist het token niet.** De interface krijgt het token
|
||||||
|
nooit te zien, dus zou opslaan hem anders weggooien.
|
||||||
|
|
||||||
# v0.8.22-beta — UniFi leest zijn databasegegevens opnieuw in
|
# v0.8.22-beta — UniFi leest zijn databasegegevens opnieuw in
|
||||||
|
|
||||||
Het linuxserver-image schrijft `/config/data/system.properties` alleen bij de
|
Het linuxserver-image schrijft `/config/data/system.properties` alleen bij de
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.8.22-beta
|
0.8.23-beta
|
||||||
|
|
|
||||||
|
|
@ -1445,6 +1445,49 @@ def api_repos_add():
|
||||||
return jsonify(ok=True)
|
return jsonify(ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/repos/<rid>", methods=["PUT"])
|
||||||
|
def api_repos_edit(rid):
|
||||||
|
"""Naam, branch of submap van een bestaande repo aanpassen.
|
||||||
|
|
||||||
|
Zonder dit kon je alleen verwijderen en opnieuw toevoegen. De id wordt
|
||||||
|
daarbij uit de URL afgeleid en krijgt er een volgnummer bij zodra hij al
|
||||||
|
bestaat — en juist die id staat in de `.serverup.json` van elke stack die
|
||||||
|
uit deze repo komt. Overstappen van de ene branch naar de andere maakte zo
|
||||||
|
stilletjes het bronsjabloon van je apps onvindbaar.
|
||||||
|
"""
|
||||||
|
d = request.json or {}
|
||||||
|
repos = list(cfg.load().get("APP_REPOS", []))
|
||||||
|
repo = next((r for r in repos
|
||||||
|
if isinstance(r, dict) and r.get("id") == rid), None)
|
||||||
|
if not repo:
|
||||||
|
return jsonify(ok=False, msg="niet gevonden"), 404
|
||||||
|
|
||||||
|
if "url" in d:
|
||||||
|
url = str(d.get("url") or "").strip()
|
||||||
|
if not url:
|
||||||
|
return jsonify(ok=False, msg="URL vereist"), 400
|
||||||
|
if not cfg.valid_repo_url(url):
|
||||||
|
return jsonify(ok=False, msg=_BAD_URL_MSG), 400
|
||||||
|
repo["url"] = url
|
||||||
|
for veld in ("name", "branch", "subdir"):
|
||||||
|
if veld in d:
|
||||||
|
repo[veld] = str(d.get(veld) or "").strip()
|
||||||
|
# Een leeg tokenveld betekent "ongewijzigd": de interface krijgt het token
|
||||||
|
# nooit te zien, dus zou opslaan hem anders wissen.
|
||||||
|
if str(d.get("token") or "").strip():
|
||||||
|
repo["token"] = d["token"].strip()
|
||||||
|
if not repo.get("branch"):
|
||||||
|
repo["branch"] = "main"
|
||||||
|
|
||||||
|
cfg.patch({"APP_REPOS": repos})
|
||||||
|
audit.log("repos", "edit", "ok", ref=rid, detail=repo.get("branch", ""),
|
||||||
|
ip=request.remote_addr)
|
||||||
|
# De cache staat nog op de oude branch; die haalt de eerstvolgende sync op.
|
||||||
|
veilig = {k: v for k, v in repo.items() if k != "token"}
|
||||||
|
veilig["has_token"] = bool((repo.get("token") or "").strip())
|
||||||
|
return jsonify(ok=True, repo=veilig)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/repos/<rid>", methods=["DELETE"])
|
@app.route("/api/repos/<rid>", methods=["DELETE"])
|
||||||
def api_repos_del(rid):
|
def api_repos_del(rid):
|
||||||
# `r["id"]` op een regel zonder id gaf hier een KeyError, dus een 500 —
|
# `r["id"]` op een regel zonder id gaf hier een KeyError, dus een 500 —
|
||||||
|
|
|
||||||
|
|
@ -1395,7 +1395,12 @@ tailwind.config = {
|
||||||
<div class="flex items-center gap-2 py-2 border-b border-bd dark:border-bd-dark last:border-0">
|
<div class="flex items-center gap-2 py-2 border-b border-bd dark:border-bd-dark last:border-0">
|
||||||
<span class="mdi mdi-source-branch text-dm dark:text-dm-dark"></span>
|
<span class="mdi mdi-source-branch text-dm dark:text-dm-dark"></span>
|
||||||
<span class="font-mono text-xs flex-1 truncate" x-text="r.name"></span>
|
<span class="font-mono text-xs flex-1 truncate" x-text="r.name"></span>
|
||||||
|
<!-- De branch bepaalt welke sjablonen je krijgt; die stilzwijgend
|
||||||
|
weglaten maakte het onmogelijk te zien waarom een app een
|
||||||
|
oude versie bleef renderen. -->
|
||||||
|
<span class="badge badge-dim font-mono !text-[10px]" x-text="r.branch || 'main'"></span>
|
||||||
<span class="text-[10px] text-t2 dark:text-t2-dark" x-text="r.subdir||''"></span>
|
<span class="text-[10px] text-t2 dark:text-t2-dark" x-text="r.subdir||''"></span>
|
||||||
|
<button @click="openRepoEdit(r)" class="btn-icon w-9 h-9" :title="t('edit')"><span class="mdi mdi-pencil-outline"></span></button>
|
||||||
<button @click="syncRepo(r.id)" class="btn-icon w-9 h-9"><span class="mdi mdi-refresh"></span></button>
|
<button @click="syncRepo(r.id)" class="btn-icon w-9 h-9"><span class="mdi mdi-refresh"></span></button>
|
||||||
<button @click="deleteRepo(r.id)" class="btn-icon w-9 h-9 !text-er hover:!border-er/40"><span class="mdi mdi-delete-outline"></span></button>
|
<button @click="deleteRepo(r.id)" class="btn-icon w-9 h-9 !text-er hover:!border-er/40"><span class="mdi mdi-delete-outline"></span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1462,6 +1467,29 @@ tailwind.config = {
|
||||||
<!-- ░░ 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 overflow-hidden">
|
<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">
|
||||||
|
|
||||||
|
<!-- Repo bewerken: alleen branch, submap en naam. De URL en de id blijven
|
||||||
|
staan, want die id staat in de metadata van elke stack uit deze repo. -->
|
||||||
|
<div x-show="modal==='edit-repo'" class="modal max-w-md">
|
||||||
|
<div class="modal-head">
|
||||||
|
<span class="mdi mdi-source-branch text-ac dark:text-ac-dark"></span>
|
||||||
|
<span x-text="t('edit_repo')">Repository bewerken</span>
|
||||||
|
<span class="font-mono text-xs text-t2 dark:text-t2-dark" x-text="repoEdit.id"></span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body space-y-3">
|
||||||
|
<div><label class="form-label" x-text="t('repo_name')">Naam</label><input class="form-input" x-model="repoEdit.name"></div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div><label class="form-label" x-text="t('repo_branch')">Branch</label><input class="form-input font-mono" x-model="repoEdit.branch"></div>
|
||||||
|
<div><label class="form-label" x-text="t('repo_subdir')">Submap</label><input class="form-input" x-model="repoEdit.subdir" placeholder="apps"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-t2 dark:text-t2-dark font-mono truncate" x-text="repoEdit.url"></p>
|
||||||
|
<p class="text-[11px] text-t2 dark:text-t2-dark" x-text="t('repo_branch_hint')"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn" @click="modal=null" x-text="t('cancel')">Annuleren</button>
|
||||||
|
<button class="btn btn-primary" @click="saveRepo()"><span class="mdi mdi-content-save-outline"></span> <span x-text="t('save')">Opslaan</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
<div class="modal-head"><span class="mdi mdi-store-plus-outline text-ac dark:text-ac-dark"></span> <span x-text="t('add_repo')">Repository toevoegen</span></div>
|
<div class="modal-head"><span class="mdi mdi-store-plus-outline text-ac dark:text-ac-dark"></span> <span x-text="t('add_repo')">Repository toevoegen</span></div>
|
||||||
|
|
@ -2638,6 +2666,7 @@ function app() {
|
||||||
job: { id: null, lines: [], status: 'idle', timer: null },
|
job: { id: null, lines: [], status: 'idle', timer: null },
|
||||||
|
|
||||||
newRepo: { name:'', url:'', branch:'main', subdir:'apps' },
|
newRepo: { name:'', url:'', branch:'main', subdir:'apps' },
|
||||||
|
repoEdit: { id:'', name:'', url:'', branch:'', subdir:'' },
|
||||||
install: {
|
install: {
|
||||||
stack: '', repo_id: '', instance: '',
|
stack: '', repo_id: '', instance: '',
|
||||||
format: 'compose', loading: false,
|
format: 'compose', loading: false,
|
||||||
|
|
@ -3732,6 +3761,21 @@ function app() {
|
||||||
this.toast('Verwijderd', 'ok');
|
this.toast('Verwijderd', 'ok');
|
||||||
this.loadAppStore();
|
this.loadAppStore();
|
||||||
},
|
},
|
||||||
|
openRepoEdit(repo) {
|
||||||
|
this.repoEdit = { id: repo.id, name: repo.name || '', url: repo.url || '',
|
||||||
|
branch: repo.branch || 'main', subdir: repo.subdir || '' };
|
||||||
|
this.modal = 'edit-repo';
|
||||||
|
},
|
||||||
|
async saveRepo() {
|
||||||
|
const d = await this.req(`/api/repos/${encodeURIComponent(this.repoEdit.id)}`,
|
||||||
|
{ method:'PUT', json: this.repoEdit });
|
||||||
|
if (!d.ok) { this.toast(d.msg || this.t('error'), 'er'); return; }
|
||||||
|
this.modal = null;
|
||||||
|
// Meteen ophalen: de cache staat nog op de vorige branch, en zonder deze
|
||||||
|
// stap zie je in de app store nog steeds de oude sjablonen.
|
||||||
|
await this.syncRepo(this.repoEdit.id);
|
||||||
|
await this.loadAppStore();
|
||||||
|
},
|
||||||
async addRepo() {
|
async addRepo() {
|
||||||
const r = await this.rpc('/api/repos', this.newRepo);
|
const r = await this.rpc('/api/repos', this.newRepo);
|
||||||
if (r.ok) { this.toast('Toegevoegd', 'ok'); this.modal=null; this.newRepo = { name:'', url:'', branch:'main', subdir:'apps' }; this.loadAppStore(); }
|
if (r.ok) { this.toast('Toegevoegd', 'ok'); this.modal=null; this.newRepo = { name:'', url:'', branch:'main', subdir:'apps' }; this.loadAppStore(); }
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
"no_stacks": "No apps found",
|
"no_stacks": "No apps found",
|
||||||
"no_images": "No images found",
|
"no_images": "No images found",
|
||||||
"no_repos": "No repository added yet — add one to see apps.",
|
"no_repos": "No repository added yet — add one to see apps.",
|
||||||
|
"edit_repo": "Edit repository",
|
||||||
|
"repo_branch_hint": "The branch decides which version of the templates you get. Saving re-fetches the repo right away; existing apps only change once you save their settings.",
|
||||||
"add_repo": "Add repository",
|
"add_repo": "Add repository",
|
||||||
"repo_name": "Name",
|
"repo_name": "Name",
|
||||||
"repo_url": "Git URL",
|
"repo_url": "Git URL",
|
||||||
|
|
@ -377,6 +379,7 @@
|
||||||
"audit_act_add": "Added",
|
"audit_act_add": "Added",
|
||||||
"audit_act_app_create": "Network created",
|
"audit_act_app_create": "Network created",
|
||||||
"audit_act_app_delete": "Network deleted",
|
"audit_act_app_delete": "Network deleted",
|
||||||
|
"audit_act_edit": "Repo edited",
|
||||||
"audit_act_apply": "Applied",
|
"audit_act_apply": "Applied",
|
||||||
"audit_act_backup": "Backed up",
|
"audit_act_backup": "Backed up",
|
||||||
"audit_act_boot_repo": "Repo at startup",
|
"audit_act_boot_repo": "Repo at startup",
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
"no_stacks": "Geen apps gevonden",
|
"no_stacks": "Geen apps gevonden",
|
||||||
"no_images": "Geen images gevonden",
|
"no_images": "Geen images gevonden",
|
||||||
"no_repos": "Nog geen repository toegevoegd — voeg er een toe om apps te zien.",
|
"no_repos": "Nog geen repository toegevoegd — voeg er een toe om apps te zien.",
|
||||||
|
"edit_repo": "Repository bewerken",
|
||||||
|
"repo_branch_hint": "De branch bepaalt welke versie van de sjablonen je krijgt. Na opslaan wordt de repo meteen opnieuw opgehaald; bestaande apps veranderen pas als je hun instellingen opslaat.",
|
||||||
"add_repo": "Repository toevoegen",
|
"add_repo": "Repository toevoegen",
|
||||||
"repo_name": "Naam",
|
"repo_name": "Naam",
|
||||||
"repo_url": "Git URL",
|
"repo_url": "Git URL",
|
||||||
|
|
@ -377,6 +379,7 @@
|
||||||
"audit_act_add": "Toegevoegd",
|
"audit_act_add": "Toegevoegd",
|
||||||
"audit_act_app_create": "Netwerk gemaakt",
|
"audit_act_app_create": "Netwerk gemaakt",
|
||||||
"audit_act_app_delete": "Netwerk verwijderd",
|
"audit_act_app_delete": "Netwerk verwijderd",
|
||||||
|
"audit_act_edit": "Repo bewerkt",
|
||||||
"audit_act_apply": "Toegepast",
|
"audit_act_apply": "Toegepast",
|
||||||
"audit_act_backup": "Backup gemaakt",
|
"audit_act_backup": "Backup gemaakt",
|
||||||
"audit_act_boot_repo": "Repo bij opstarten",
|
"audit_act_boot_repo": "Repo bij opstarten",
|
||||||
|
|
|
||||||
|
|
@ -280,3 +280,80 @@ def test_verwijderen_haalt_alle_regels_met_die_id_weg(client, env):
|
||||||
assert r.status_code == 200, r.get_json()
|
assert r.status_code == 200, r.get_json()
|
||||||
over = core.load()["APP_REPOS"]
|
over = core.load()["APP_REPOS"]
|
||||||
assert [x.get("id") for x in over] == ["blijft", None], over
|
assert [x.get("id") for x in over] == ["blijft", None], over
|
||||||
|
|
||||||
|
|
||||||
|
# ── Een bestaande repo aanpassen ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_branch_van_bestaande_repo_wijzigen(client, env):
|
||||||
|
"""De id van een repo staat in de metadata van elke stack die eruit komt.
|
||||||
|
|
||||||
|
Wie van branch wilde wisselen kon alleen verwijderen en opnieuw toevoegen;
|
||||||
|
de id kreeg er dan een volgnummer bij en het bronsjabloon van bestaande
|
||||||
|
apps was ineens onvindbaar.
|
||||||
|
"""
|
||||||
|
csrf = login(client)
|
||||||
|
client.post("/api/repos",
|
||||||
|
json={"url": "https://example.com/eigen-apps.git", "branch": "main",
|
||||||
|
"subdir": "apps"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
|
||||||
|
r = client.put("/api/repos/eigen-apps", json={"branch": "dev"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200, r.get_json()
|
||||||
|
|
||||||
|
repo = next(x for x in client.get("/api/repos").get_json()
|
||||||
|
if x["id"] == "eigen-apps")
|
||||||
|
assert repo["branch"] == "dev"
|
||||||
|
# De rest blijft staan; alleen wat je meestuurt verandert.
|
||||||
|
assert repo["subdir"] == "apps"
|
||||||
|
assert repo["url"] == "https://example.com/eigen-apps.git"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bewerken_van_onbekende_repo(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
r = client.put("/api/repos/bestaatniet", json={"branch": "dev"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_bewerken_wist_een_bestaand_token_niet(client, env):
|
||||||
|
"""De interface krijgt het token nooit te zien, dus stuurt hij het leeg
|
||||||
|
terug. Dat mag geen repo zonder toegang opleveren."""
|
||||||
|
csrf = login(client)
|
||||||
|
client.post("/api/repos",
|
||||||
|
json={"url": "https://example.com/prive.git", "token": "s3cr3t-token"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
|
||||||
|
r = client.put("/api/repos/prive", json={"branch": "dev", "token": ""},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "s3cr3t-token" not in r.get_data(as_text=True)
|
||||||
|
|
||||||
|
from core import config as _cfg # noqa: F401
|
||||||
|
import core
|
||||||
|
repo = next(x for x in core.load()["APP_REPOS"] if x["id"] == "prive")
|
||||||
|
assert repo["token"] == "s3cr3t-token"
|
||||||
|
assert repo["branch"] == "dev"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bewerken_weigert_een_ongeldige_url(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
client.post("/api/repos", json={"url": "https://example.com/proef.git"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
r = client.put("/api/repos/proef", json={"url": 'ext::sh -c "id"'},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_viewer_mag_geen_repo_bewerken(client, env):
|
||||||
|
csrf = login(client)
|
||||||
|
client.post("/api/repos", json={"url": "https://example.com/proef.git"},
|
||||||
|
headers={"X-CSRF-Token": csrf})
|
||||||
|
client.post("/api/auth/users",
|
||||||
|
json={"username": "kijker", "password": "eenlangwachtwoord",
|
||||||
|
"role": "viewer"}, headers={"X-CSRF-Token": csrf})
|
||||||
|
kijker = client.post("/api/auth/login",
|
||||||
|
json={"username": "kijker", "password": "eenlangwachtwoord"}
|
||||||
|
).get_json()["csrf_token"]
|
||||||
|
assert client.put("/api/repos/proef", json={"branch": "dev"},
|
||||||
|
headers={"X-CSRF-Token": kijker}).status_code == 403
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue