diff --git a/CLAUDE.md b/CLAUDE.md index a3aa255..37ed659 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,8 +42,11 @@ Prod läuft als **TrueNAS SCALE Custom-App** `gerbilmanager` auf dem Host **`tru - **Sichtbar unter Apps**, weil via `midclt call app.create {custom_app:true, app_name:"gerbilmanager", custom_compose_config_string:}` angelegt — **nicht** via `docker compose` (das taucht in Apps NICHT auf). Container: `ix-gerbilmanager-{db,api,frontend,backup}-1`. -- **4 Dienste:** `db` (postgres:18) · `api` (.NET, EF-Migrationen laufen beim Start) · `frontend` - (nginx: SPA + `/api`-Proxy → `api:8080`) · `backup` (täglich 03:00 pg_dump + Foto-Archiv, Shell-Scheduler). +- **5 Dienste:** `db` (postgres:18) · `api` (.NET, EF-Migrationen laufen beim Start) · `frontend` + (nginx: SPA + `/api`-Proxy → `api:8080`, Host-Port 8090) · `publicsite` (nginx, **nur intern**, + Host-Port **8081** — serviert statisches HTML aus dem `publicsite`-Volume, das die API via + `POST /api/publish` schreibt; ohne Publish leer → 404) · `backup` (täglich 03:00 pg_dump + + Foto-Archiv, Shell-Scheduler). - **App-Home auf dem Pool:** `/mnt/JailStorage/DockerVolumes/gerbilmanager/` mit `pgdata/ photos/ keys/ backups/ scripts/ publicsite/` und `deploy/truenas/{custom-app.compose.yaml,.env,scripts}`. Images aus der Gitea-Registry `git.rismer.de/gulum/gerbilmanager-{api,frontend}:latest`. diff --git a/deploy/truenas/custom-app.compose.yaml b/deploy/truenas/custom-app.compose.yaml index cc71ab7..e4a67bf 100644 --- a/deploy/truenas/custom-app.compose.yaml +++ b/deploy/truenas/custom-app.compose.yaml @@ -79,6 +79,22 @@ services: api: condition: service_healthy + # --- Oeffentliche Seite (WEB-2), vorerst nur INTERN gehostet --- + # Serviert NUR statisches HTML aus dem publicsite-Volume (live/), das die API via + # POST /api/publish schreibt. Kein Proxy auf api/frontend. Solange nichts publiziert + # wurde, ist das Volume leer -> 404 (erwartet). Extern (Domain/TLS) ist ein spaeterer + # Schritt ueber Julians Reverse-Proxy (siehe deploy/truenas/vhost-snippet.conf). + publicsite: + image: nginx:alpine + restart: unless-stopped + ports: + - "__PUBLICSITE_PORT__:80" + volumes: + - /mnt/JailStorage/DockerVolumes/gerbilmanager/publicsite:/usr/share/nginx/html:ro + - /mnt/JailStorage/DockerVolumes/gerbilmanager/deploy/truenas/nginx/publicsite.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - api + # --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) --- backup: image: postgres:18 diff --git a/deploy/truenas/scripts/truenas-deploy.sh b/deploy/truenas/scripts/truenas-deploy.sh index 6f0ea85..72c7bc7 100644 --- a/deploy/truenas/scripts/truenas-deploy.sh +++ b/deploy/truenas/scripts/truenas-deploy.sh @@ -38,6 +38,7 @@ set -a; . "$ENV_FILE"; set +a export REGISTRY="${REGISTRY:-git.rismer.de/gulum}" export TAG="${TAG:-latest}" export PORT="${PORT:-8090}" +export PUBLICSITE_PORT="${PUBLICSITE_PORT:-8081}" log "=== Deploy $APP_NAME (TAG=$TAG, PORT=$PORT) ===" @@ -54,36 +55,49 @@ log "Ziehe Images ..." docker pull "$REGISTRY/gerbilmanager-api:$TAG" || fail "docker pull api fehlgeschlagen" docker pull "$REGISTRY/gerbilmanager-frontend:$TAG" || fail "docker pull frontend fehlgeschlagen" -# --- Vorlage rendern + midclt-Payload bauen (python: sichere Escapes) --- -PAYLOAD_FILE="$(mktemp)" -trap 'rm -f "$PAYLOAD_FILE"' EXIT -python3 - "$TEMPLATE" > "$PAYLOAD_FILE" <<'PY' -import sys, os, json +# --- Vorlage rendern (python: sichere Substitution) --- +RENDERED="$(mktemp)" +trap 'rm -f "$RENDERED"' EXIT +python3 - "$TEMPLATE" > "$RENDERED" <<'PY' +import sys, os tmpl = open(sys.argv[1], encoding="utf-8").read() repl = { "__POSTGRES_PASSWORD__": os.environ["POSTGRES_PASSWORD"], "__REGISTRY__": os.environ.get("REGISTRY", "git.rismer.de/gulum"), "__TAG__": os.environ.get("TAG", "latest"), "__PORT__": os.environ.get("PORT", "8090"), + "__PUBLICSITE_PORT__": os.environ.get("PUBLICSITE_PORT", "8081"), "__AI_BASEURL__": os.environ.get("AI__BaseUrl", ""), "__AI_APIKEY__": os.environ.get("AI__ApiKey", ""), "__AI_MODEL__": os.environ.get("AI__Model", "gemini-flash-latest"), } for k, v in repl.items(): tmpl = tmpl.replace(k, v) -print(json.dumps({"custom_app": True, "app_name": "gerbilmanager", - "custom_compose_config_string": tmpl})) +sys.stdout.write(tmpl) PY +# midclt-Payload bauen: create braucht app_name+custom_app, update nur die Compose. +build_payload() { # $1 = create|update + python3 - "$RENDERED" "$1" <<'PY' +import sys, json +compose = open(sys.argv[1], encoding="utf-8").read() +if sys.argv[2] == "create": + print(json.dumps({"custom_app": True, "app_name": "gerbilmanager", + "custom_compose_config_string": compose})) +else: + print(json.dumps({"custom_compose_config_string": compose})) +PY +} + # --- Existiert die App schon? --- EXISTS="$(midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" 2>/dev/null | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0)" if [ "$EXISTS" -ge 1 ]; then - log "App existiert -> redeploy (frische Images sind bereits gezogen)" - midclt call app.redeploy "$APP_NAME" >/dev/null || fail "app.redeploy fehlgeschlagen" + log "App existiert -> app.update (Compose-Aenderungen uebernehmen, frische Images gezogen)" + midclt call app.update "$APP_NAME" "$(build_payload update)" >/dev/null || fail "app.update fehlgeschlagen" else log "App fehlt -> app.create (Custom App)" - midclt call app.create "$(cat "$PAYLOAD_FILE")" >/dev/null || fail "app.create fehlgeschlagen" + midclt call app.create "$(build_payload create)" >/dev/null || fail "app.create fehlgeschlagen" fi # --- Auf Gesundheit warten (Frontend proxyt /api/health -> API) ---