feat(apps): toon uit welk sjabloon een app gerenderd wordt
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m17s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m17s
Sjablonen komen uit de git-cache van een app-repo. Loopt die achter, dan trekt 'Instellingen wijzigen' een oude versie over een werkende app heen zonder dat iets dat zegt — precies wat er bij UniFi gebeurde. - git.cache_status geeft commit, datum, synchronisatiemoment en de VERSION uit de cache terug. - /api/repos en /api/stacks/<naam>/config sturen die stand mee, met een outdated-vlag als de repo-VERSION ouder is dan de draaiende versie. - Het wijzigscherm toont repo, branch en hoe lang geleden er opgehaald is, en waarschuwt bij een achterlopende cache. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
This commit is contained in:
parent
c658418b10
commit
b40a6a5a4c
8 changed files with 226 additions and 4 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,3 +1,18 @@
|
|||
# v0.8.25-beta — Zichtbaar uit welk sjabloon een app komt
|
||||
|
||||
Sjablonen komen uit de git-cache van een app-repo, niet uit het image van
|
||||
Server Up. Die twee kunnen ver uit elkaar lopen: staat de repo op een branch
|
||||
die achterloopt, dan rendert 'Instellingen wijzigen' een oud sjabloon over een
|
||||
werkende app heen — en daar was aan niets te zien.
|
||||
|
||||
- **Het wijzigscherm noemt zijn bron.** Repo, branch en hoe lang geleden er
|
||||
gesynchroniseerd is, boven de velden waar je op opslaan drukt.
|
||||
- **Een achterlopende repo waarschuwt.** Levert de repo zelf een `VERSION` mee
|
||||
(zoals deze) en is die ouder dan de draaiende versie, dan zegt het scherm dat
|
||||
met de versie erbij.
|
||||
- **De repolijst toont de stand.** Commit en synchronisatiemoment per repo,
|
||||
zodat je niet hoeft te gokken of een sync nog nodig is.
|
||||
|
||||
# v0.8.24-beta — Koppelen laat je database met rust
|
||||
|
||||
Koppelen aan het gedeelde netwerk hing élke service van een stack eraan, ook de
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.8.24-beta
|
||||
0.8.25-beta
|
||||
|
|
|
|||
|
|
@ -1115,7 +1115,25 @@ def api_stack_config(name):
|
|||
app_networks=meta.get("app_networks") or [],
|
||||
app_services=meta.get("app_services") or [],
|
||||
source=meta.get("source", ""), repo_id=meta.get("repo_id", ""),
|
||||
name=meta.get("name", name))
|
||||
name=meta.get("name", name),
|
||||
# Waar dit sjabloon vandaan komt. Opslaan rendert de app
|
||||
# opnieuw uit déze cache; loopt die achter, dan haal je een
|
||||
# oude versie over je werkende stack heen.
|
||||
source_repo=_sjabloon_herkomst(meta.get("repo_id", "")))
|
||||
|
||||
|
||||
def _sjabloon_herkomst(rid: str) -> dict:
|
||||
"""Repo, branch en stand van de cache waar een sjabloon uit komt."""
|
||||
if not rid:
|
||||
return {}
|
||||
repo = next((r for r in cfg.load().get("APP_REPOS", [])
|
||||
if isinstance(r, dict) and r.get("id") == rid), None)
|
||||
if not repo:
|
||||
return {"id": rid, "missing": True}
|
||||
stand = git.cache_status(rid)
|
||||
stand["outdated"] = _cache_loopt_achter(stand.get("version", ""))
|
||||
return {"id": rid, "name": repo.get("name") or rid,
|
||||
"branch": repo.get("branch") or "main", **stand}
|
||||
|
||||
|
||||
@app.route("/api/stacks/<name>/reconfigure", methods=["POST"])
|
||||
|
|
@ -1432,10 +1450,28 @@ def api_repos():
|
|||
if r["id"] in gezien:
|
||||
continue
|
||||
gezien.add(r["id"])
|
||||
result.append({**r, "cloned": git.cache_dir(r["id"]).exists()})
|
||||
# Mét de stand van de cache: welke commit, van wanneer, en loopt de
|
||||
# meegeleverde VERSION achter op wat hier draait? Zonder dat is niet te
|
||||
# zien dat een app een oud sjabloon rendert.
|
||||
stand = git.cache_status(r["id"])
|
||||
stand["outdated"] = _cache_loopt_achter(stand.get("version", ""))
|
||||
result.append({**r, "cloned": git.cache_dir(r["id"]).exists(),
|
||||
"cache": stand})
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
def _cache_loopt_achter(repo_versie: str) -> bool:
|
||||
"""Is de VERSION in de git-cache ouder dan de draaiende Server Up?
|
||||
|
||||
Alleen zinvol voor repo's die zelf een VERSION meeleveren; voor de rest
|
||||
zegt dit niets en blijft het antwoord False.
|
||||
"""
|
||||
if not repo_versie:
|
||||
return False
|
||||
hier, daar = updater._parse(VERSION), updater._parse(repo_versie)
|
||||
return bool(hier and daar and daar < hier)
|
||||
|
||||
|
||||
@app.route("/api/repos", methods=["POST"])
|
||||
def api_repos_add():
|
||||
d = request.json or {}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,44 @@ def cache_dir(repo_id: str) -> Path:
|
|||
return CACHE / repo_id
|
||||
|
||||
|
||||
def cache_status(repo_id: str) -> dict:
|
||||
"""Welke versie van de sjablonen staat er in de cache van deze repo?
|
||||
|
||||
Sjablonen komen hiervandaan en niet uit het image van Server Up. Die twee
|
||||
kunnen ver uit elkaar lopen: staat de repo op een branch die achterloopt,
|
||||
dan rendert een app een oud sjabloon terwijl de code splinternieuw is — en
|
||||
daar is aan niets te zien.
|
||||
"""
|
||||
d = cache_dir(repo_id)
|
||||
if not (d / ".git").exists():
|
||||
return {"cloned": False}
|
||||
uit: dict = {"cloned": True}
|
||||
try:
|
||||
r = subprocess.run(["git", "log", "-1", "--format=%h%x1f%cI%x1f%s"],
|
||||
cwd=str(d), capture_output=True, text=True, timeout=10)
|
||||
if r.returncode == 0 and "\x1f" in r.stdout:
|
||||
sha, datum, onderwerp = r.stdout.strip().split("\x1f", 2)
|
||||
uit.update(commit=sha, commit_date=datum, subject=onderwerp[:120])
|
||||
except Exception:
|
||||
pass
|
||||
# Wanneer is er voor het laatst opgehaald? FETCH_HEAD wordt bij elke fetch
|
||||
# aangeraakt; bij een verse kloon bestaat hij nog niet.
|
||||
for naam in ("FETCH_HEAD", "HEAD"):
|
||||
p = d / ".git" / naam
|
||||
if p.exists():
|
||||
uit["synced_at"] = p.stat().st_mtime
|
||||
break
|
||||
# Repo's die zelf een VERSION meeleveren (zoals deze) verraden daarmee of de
|
||||
# cache achterloopt op de draaiende versie.
|
||||
versie = d / "VERSION"
|
||||
if versie.is_file():
|
||||
try:
|
||||
uit["version"] = versie.read_text(encoding="utf-8").strip()[:40]
|
||||
except Exception:
|
||||
pass
|
||||
return uit
|
||||
|
||||
|
||||
def clone_or_pull(repo: dict, log_fn=None) -> tuple[bool, str]:
|
||||
_ensure_git_safe()
|
||||
url = repo.get("url", "").strip()
|
||||
|
|
|
|||
|
|
@ -1392,9 +1392,17 @@ tailwind.config = {
|
|||
<summary class="px-5 py-4 cursor-pointer font-bold text-wh dark:text-wh-dark text-sm flex items-center gap-2.5"><span class="mdi mdi-source-branch text-ac dark:text-ac-dark text-lg"></span> Git Repositories</summary>
|
||||
<div class="px-5 pb-5 space-y-2">
|
||||
<template x-for="r in repos" :key="r.id">
|
||||
<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 flex-wrap">
|
||||
<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>
|
||||
<!-- De stand van de cache: hieruit komen de sjablonen, en die
|
||||
kan ver achterlopen op de draaiende versie. -->
|
||||
<span x-show="(r.cache||{}).outdated" class="badge badge-warn"
|
||||
:title="t('cache_outdated_hint')" x-text="(r.cache||{}).version"></span>
|
||||
<span x-show="(r.cache||{}).commit && !(r.cache||{}).outdated"
|
||||
class="text-[10px] font-mono text-dm dark:text-dm-dark"
|
||||
:title="(r.cache||{}).subject"
|
||||
x-text="(r.cache||{}).commit + ' · ' + geleden((r.cache||{}).synced_at)"></span>
|
||||
<!-- De branch bepaalt welke sjablonen je krijgt; die stilzwijgend
|
||||
weglaten maakte het onmogelijk te zien waarom een app een
|
||||
oude versie bleef renderen. -->
|
||||
|
|
@ -1765,6 +1773,27 @@ tailwind.config = {
|
|||
<span x-text="t('reconfigure_warning')"></span>
|
||||
</p>
|
||||
|
||||
<!-- Waar het sjabloon vandaan komt. Opslaan rendert de app opnieuw uit
|
||||
déze cache: loopt die achter, dan trek je een oude versie over een
|
||||
werkende stack heen zonder dat iets dat zegt. -->
|
||||
<div x-show="install.mode === 'edit' && install.source_repo && install.source_repo.id"
|
||||
class="text-[11px] rounded-xl border px-3 py-2 flex items-start gap-2"
|
||||
:class="(install.source_repo||{}).outdated
|
||||
? 'border-wr/40 bg-wr/5 text-wr'
|
||||
: 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark'">
|
||||
<span class="mdi shrink-0"
|
||||
:class="(install.source_repo||{}).outdated ? 'mdi-alert-outline' : 'mdi-source-branch'"></span>
|
||||
<span class="min-w-0">
|
||||
<span x-text="t('template_from', {
|
||||
repo: (install.source_repo||{}).name || (install.source_repo||{}).id,
|
||||
branch: (install.source_repo||{}).branch || 'main'})"></span>
|
||||
<span x-show="(install.source_repo||{}).synced_at"
|
||||
x-text="' · ' + t('synced_ago', {ago: geleden((install.source_repo||{}).synced_at)})"></span>
|
||||
<span x-show="(install.source_repo||{}).outdated" class="block mt-0.5"
|
||||
x-text="t('cache_outdated', {version: (install.source_repo||{}).version, current: version})"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Uitleg bij het veld waar je op het info-icoon klikte -->
|
||||
<div x-show="fieldInfo" x-cloak
|
||||
class="rounded-xl border border-ac/30 bg-ac/5 px-3.5 py-3">
|
||||
|
|
@ -4090,6 +4119,7 @@ function app() {
|
|||
repo_id: d.repo_id || '',
|
||||
app_networks: d.app_networks || (d.connected ? [this.sharedNetwork] : []),
|
||||
app_services: d.app_services || [], service_info: [],
|
||||
source_repo: d.source_repo || {},
|
||||
};
|
||||
await this.loadAppNets(stack);
|
||||
this.fieldInfo = null;
|
||||
|
|
@ -4276,6 +4306,17 @@ function app() {
|
|||
if (!ts) return '';
|
||||
return new Date(ts * 1000).toLocaleString();
|
||||
},
|
||||
// "3 dagen geleden" leest bij een synchronisatie prettiger dan een datum:
|
||||
// de vraag is niet wannéér het was, maar of het lang geleden is.
|
||||
geleden(ts) {
|
||||
if (!ts) return '';
|
||||
const sec = Math.max(0, Date.now() / 1000 - ts);
|
||||
const stappen = [[86400, 'd'], [3600, 'u'], [60, 'm']];
|
||||
for (const [n, letter] of stappen) {
|
||||
if (sec >= n) return Math.floor(sec / n) + letter;
|
||||
}
|
||||
return this.t('just_now');
|
||||
},
|
||||
|
||||
// Bijwerken: de container vervangt zichzelf, dus na het startsein is deze
|
||||
// pagina zijn server kwijt. We pollen /healthz tot de nieuwe versie leeft.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
"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.",
|
||||
"template_from": "Template from {repo} ({branch})",
|
||||
"synced_ago": "fetched {ago} ago",
|
||||
"cache_outdated": "This repo is on {version} while Server Up runs {current}. Sync it or pick another branch — otherwise saving renders an older template over your app.",
|
||||
"cache_outdated_hint": "The templates in this repo lag behind the running version of Server Up.",
|
||||
"just_now": "just now",
|
||||
"add_repo": "Add repository",
|
||||
"repo_name": "Name",
|
||||
"repo_url": "Git URL",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
"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.",
|
||||
"template_from": "Sjabloon uit {repo} ({branch})",
|
||||
"synced_ago": "{ago} geleden opgehaald",
|
||||
"cache_outdated": "Deze repo staat op {version} terwijl Server Up {current} draait. Synchroniseer hem of kies een andere branch — anders rendert opslaan een ouder sjabloon over je app heen.",
|
||||
"cache_outdated_hint": "De sjablonen in deze repo lopen achter op de draaiende versie van Server Up.",
|
||||
"just_now": "zojuist",
|
||||
"add_repo": "Repository toevoegen",
|
||||
"repo_name": "Naam",
|
||||
"repo_url": "Git URL",
|
||||
|
|
|
|||
|
|
@ -357,3 +357,85 @@ def test_viewer_mag_geen_repo_bewerken(client, env):
|
|||
).get_json()["csrf_token"]
|
||||
assert client.put("/api/repos/proef", json={"branch": "dev"},
|
||||
headers={"X-CSRF-Token": kijker}).status_code == 403
|
||||
|
||||
|
||||
# ── Herkomst van een sjabloon ────────────────────────────────────────────────
|
||||
|
||||
def test_repolijst_toont_de_stand_van_de_cache(client, env, tmp_path, monkeypatch):
|
||||
"""Sjablonen komen uit de git-cache, niet uit het image. Loopt die achter,
|
||||
dan rendert een app een oude versie terwijl de code nieuw is — en daar was
|
||||
aan niets te zien."""
|
||||
import shutil as _shutil
|
||||
from core import git
|
||||
if not _shutil.which("git"):
|
||||
pytest.skip("git niet aanwezig")
|
||||
csrf = login(client)
|
||||
bron = _repo_met_twee_takken(tmp_path)
|
||||
(git.cache_dir("proef")).parent.mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setattr("core.valid_repo_url", lambda u: True)
|
||||
client.post("/api/repos", json={"url": f"file://{bron}", "id": "proef",
|
||||
"branch": "main"},
|
||||
headers={"X-CSRF-Token": csrf})
|
||||
ok, msg = git.clone_or_pull({"id": "proef", "url": f"file://{bron}",
|
||||
"branch": "main"})
|
||||
assert ok, msg
|
||||
|
||||
repo = next(r for r in client.get("/api/repos").get_json() if r["id"] == "proef")
|
||||
assert repo["cache"]["cloned"] is True
|
||||
assert repo["cache"]["commit"]
|
||||
assert repo["cache"]["synced_at"] > 0
|
||||
|
||||
|
||||
def test_achterlopende_repo_wordt_gemeld(client, env, tmp_path, monkeypatch):
|
||||
"""Precies het geval dat drie releases lang onzichtbaar bleef: de repo staat
|
||||
op een oudere versie dan Server Up zelf."""
|
||||
import shutil as _shutil
|
||||
from core import git
|
||||
if not _shutil.which("git"):
|
||||
pytest.skip("git niet aanwezig")
|
||||
csrf = login(client)
|
||||
bron = _repo_met_twee_takken(tmp_path)
|
||||
(bron / "VERSION").write_text("0.0.01-beta\n")
|
||||
import subprocess as sp
|
||||
sp.run(["git", "add", "-A"], cwd=bron, check=True, capture_output=True)
|
||||
sp.run(["git", "commit", "-qm", "versie"], cwd=bron, check=True,
|
||||
capture_output=True)
|
||||
monkeypatch.setattr("core.valid_repo_url", lambda u: True)
|
||||
client.post("/api/repos", json={"url": f"file://{bron}", "id": "oud",
|
||||
"branch": "main"},
|
||||
headers={"X-CSRF-Token": csrf})
|
||||
git.clone_or_pull({"id": "oud", "url": f"file://{bron}", "branch": "main"})
|
||||
|
||||
repo = next(r for r in client.get("/api/repos").get_json() if r["id"] == "oud")
|
||||
assert repo["cache"]["version"] == "0.0.01-beta"
|
||||
assert repo["cache"]["outdated"] is True
|
||||
|
||||
|
||||
def test_repo_zonder_versiebestand_heet_niet_verouderd(env):
|
||||
import app as app_module
|
||||
assert app_module._cache_loopt_achter("") is False
|
||||
assert app_module._cache_loopt_achter("geen-versie") is False
|
||||
assert app_module._cache_loopt_achter("99.9.99") is False
|
||||
|
||||
|
||||
def test_wijzigscherm_kent_de_herkomst_van_het_sjabloon(client, env):
|
||||
"""Het scherm waar je op opslaan drukt hoort te zeggen waaruit hij rendert."""
|
||||
import json as _json
|
||||
csrf = login(client)
|
||||
d = env["lib"] / "web"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
(d / "docker-compose.yml").write_text("services:\n web:\n image: nginx\n")
|
||||
(d / ".serverup.json").write_text(_json.dumps({
|
||||
"source": "keuzeapp", "repo_id": "server-up", "values": {"port": 80}}))
|
||||
|
||||
from core import git
|
||||
wortel = git.cache_dir("server-up") / "apps" / "keuzeapp"
|
||||
(wortel / "files").mkdir(parents=True, exist_ok=True)
|
||||
(wortel / "template.json").write_text(_json.dumps({
|
||||
"kind": "compose", "metadata": {"name": "keuzeapp"}, "variables": []}))
|
||||
(wortel / "files" / "compose.yaml").write_text("services:\n web:\n image: nginx\n")
|
||||
|
||||
d2 = client.get("/api/stacks/web/config").get_json()
|
||||
assert d2["ok"] is True
|
||||
assert d2["source_repo"]["id"] == "server-up"
|
||||
assert d2["source_repo"]["branch"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue