From 4407bbfbb8c98445afee0df8de0b8ced94eee6eb Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 7 May 2026 19:51:19 +0200 Subject: [PATCH] factory-defaults: CLI-Sync zwischen dev und prod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei kleine Bash-Wrapper im Repo-Root, die den vorhandenen Export- und Import-Endpoint per curl ansteuern und damit den Hin- und Her-Transfer von Stammdaten + HTML-Templates zwischen Instanzen ohne Browser ermöglichen. ./factory-export.sh # ZIP nach factory-exports/ ./factory-import.sh # nimmt jüngste ZIP automatisch ./factory-import.sh path/zur.zip # explizit Konfigurierbar via OPENCRM_URL / OPENCRM_EMAIL / OPENCRM_PASSWORD; ohne PASSWORD wird interaktiv abgefragt. Workflow: prod erweitert Anbieter → ./factory-export.sh → scp → dev ./factory-import.sh – funktioniert in beide Richtungen. `factory-exports/` ist gitignored (nur .gitkeep getrackt). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 ++ docs/todo.md | 11 +++++ factory-export.sh | 64 ++++++++++++++++++++++++++ factory-exports/.gitkeep | 0 factory-import.sh | 97 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100755 factory-export.sh create mode 100644 factory-exports/.gitkeep create mode 100755 factory-import.sh diff --git a/.gitignore b/.gitignore index 4f761367..a727d389 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ data/factory-defaults/* !data/factory-defaults/.gitkeep data/backups/* !data/backups/.gitkeep + +# Factory-Defaults-Drop-Box (Export-ZIPs zwischen dev/prod hin und her) +factory-exports/* +!factory-exports/.gitkeep diff --git a/docs/todo.md b/docs/todo.md index d95c6207..79b26dc6 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod)** + - `./factory-export.sh` zieht eine ZIP per API in `factory-exports/` + (gitignored Drop-Box). + - `./factory-import.sh [zip]` lädt die ZIP per API in eine andere Instanz + – ohne Argument wählt es die jüngste ZIP automatisch. + - Konfigurierbar per Env: `OPENCRM_URL`, `OPENCRM_EMAIL`, + `OPENCRM_PASSWORD` (sonst interaktive Abfrage). + - Use-Case: Anbieter/Tarife auf prod erweitert? `./factory-export.sh` auf + prod, `scp` ins dev, `./factory-import.sh` lokal – fertig. Geht in + beide Richtungen. + - [x] **🚀 Auto-Seed: Werkseinstellungen beim Erst-Deploy** - Inhalt von `backend/factory-defaults/` wird via Dockerfile als `/app/factory-defaults-builtin/` ins Image gebrannt. diff --git a/factory-export.sh b/factory-export.sh new file mode 100755 index 00000000..bb0347e5 --- /dev/null +++ b/factory-export.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Factory-Defaults-Export – holt eine ZIP vom laufenden OpenCRM und legt sie +# in ./factory-exports/ ab. Dieselbe ZIP, die du auch über die UI bekommst. +# +# Workflow: +# ./factory-export.sh # default: localhost:3010, admin@admin.com +# OPENCRM_URL=https://crm.example.de \ +# OPENCRM_EMAIL=admin@example.de \ +# ./factory-export.sh # gegen die Prod-Instanz +# +# Optional: +# OPENCRM_PASSWORD=… (sonst wird interaktiv abgefragt) +# +# Die ZIP ist gitignored – du kannst sie via scp transferieren und mit +# ./factory-import.sh auf der anderen Seite einspielen. + +set -euo pipefail + +URL="${OPENCRM_URL:-http://localhost:3010}" +EMAIL="${OPENCRM_EMAIL:-admin@admin.com}" +PASSWORD="${OPENCRM_PASSWORD:-}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXPORT_DIR="$REPO_ROOT/factory-exports" +mkdir -p "$EXPORT_DIR" + +if [ -z "$PASSWORD" ]; then + read -r -s -p "Passwort für $EMAIL @ $URL: " PASSWORD + echo +fi + +echo "→ Login als $EMAIL @ $URL" +LOGIN_RESPONSE="$(curl -sS -X POST "$URL/api/auth/login" \ + -H 'Content-Type: application/json' \ + --data-raw "$(E="$EMAIL" P="$PASSWORD" python3 -c 'import json,os;print(json.dumps({"email":os.environ["E"],"password":os.environ["P"]}))')")" + +TOKEN="$(printf '%s' "$LOGIN_RESPONSE" | python3 -c 'import json,sys;d=json.load(sys.stdin);print((d.get("data") or {}).get("token","") or d.get("token",""))')" + +if [ -z "$TOKEN" ]; then + echo "✗ Login fehlgeschlagen. Antwort:" + echo "$LOGIN_RESPONSE" | head -c 500 + echo + exit 1 +fi + +TIMESTAMP="$(date +%Y-%m-%d-%H%M)" +DEST="$EXPORT_DIR/factory-defaults-$TIMESTAMP.zip" + +echo "→ Lade ZIP nach $DEST" +HTTP_CODE="$(curl -sS -o "$DEST" -w '%{http_code}' \ + -H "Authorization: Bearer $TOKEN" \ + "$URL/api/factory-defaults/export")" + +if [ "$HTTP_CODE" != "200" ]; then + echo "✗ Export-Endpoint antwortete mit HTTP $HTTP_CODE" + rm -f "$DEST" + exit 1 +fi + +SIZE_KB="$(du -k "$DEST" | cut -f1)" +echo "✓ Export erfolgreich: $DEST (${SIZE_KB} KB)" +echo +echo "Inhalt:" +unzip -l "$DEST" | sed 's/^/ /' diff --git a/factory-exports/.gitkeep b/factory-exports/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/factory-import.sh b/factory-import.sh new file mode 100755 index 00000000..e1cf3d36 --- /dev/null +++ b/factory-import.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Factory-Defaults-Import – pflegt eine ZIP in eine OpenCRM-Instanz ein. +# Idempotent (upserts pro Kategorie, nichts wird gelöscht). +# +# Aufruf: +# ./factory-import.sh ./factory-exports/factory-defaults-2026-05-07-1923.zip +# ./factory-import.sh # ohne Argument: nimmt automatisch +# # die jüngste ZIP aus factory-exports/ +# +# ENV (wie factory-export.sh): +# OPENCRM_URL (default http://localhost:3010) +# OPENCRM_EMAIL (default admin@admin.com) +# OPENCRM_PASSWORD (sonst interaktiv) + +set -euo pipefail + +URL="${OPENCRM_URL:-http://localhost:3010}" +EMAIL="${OPENCRM_EMAIL:-admin@admin.com}" +PASSWORD="${OPENCRM_PASSWORD:-}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXPORT_DIR="$REPO_ROOT/factory-exports" + +ZIP_PATH="${1:-}" +if [ -z "$ZIP_PATH" ]; then + # Jüngste ZIP automatisch wählen + ZIP_PATH="$(ls -1t "$EXPORT_DIR"/*.zip 2>/dev/null | head -1 || true)" + if [ -z "$ZIP_PATH" ]; then + echo "✗ Keine ZIP angegeben und keine in $EXPORT_DIR/ gefunden." + echo " Aufruf: ./factory-import.sh " + exit 1 + fi + echo "→ Keine ZIP angegeben – nehme jüngste aus $EXPORT_DIR/:" + echo " $(basename "$ZIP_PATH")" +fi + +if [ ! -f "$ZIP_PATH" ]; then + echo "✗ Datei nicht gefunden: $ZIP_PATH" + exit 1 +fi + +if [ -z "$PASSWORD" ]; then + read -r -s -p "Passwort für $EMAIL @ $URL: " PASSWORD + echo +fi + +echo "→ Login als $EMAIL @ $URL" +LOGIN_RESPONSE="$(curl -sS -X POST "$URL/api/auth/login" \ + -H 'Content-Type: application/json' \ + --data-raw "$(E="$EMAIL" P="$PASSWORD" python3 -c 'import json,os;print(json.dumps({"email":os.environ["E"],"password":os.environ["P"]}))')")" + +TOKEN="$(printf '%s' "$LOGIN_RESPONSE" | python3 -c 'import json,sys;d=json.load(sys.stdin);print((d.get("data") or {}).get("token","") or d.get("token",""))')" + +if [ -z "$TOKEN" ]; then + echo "✗ Login fehlgeschlagen. Antwort:" + echo "$LOGIN_RESPONSE" | head -c 500 + echo + exit 1 +fi + +echo "→ Upload + Import: $(basename "$ZIP_PATH")" +RESPONSE="$(curl -sS -X POST "$URL/api/factory-defaults/import" \ + -H "Authorization: Bearer $TOKEN" \ + -F "zip=@$ZIP_PATH")" + +# Hübsch ausgeben + auf success prüfen +if printf '%s' "$RESPONSE" | python3 -c ' +import json, sys +r = json.load(sys.stdin) +if not r.get("success"): + print("✗ Import fehlgeschlagen:", r.get("error", "(unbekannt)")) + sys.exit(1) +d = r.get("data", {}) +print("✓ Import erfolgreich:") +for label, key in [ + ("Anbieter", "providers"), + ("Tarife", "tariffs"), + ("Kündigungsfristen", "cancellationPeriods"), + ("Laufzeiten", "contractDurations"), + ("Vertragskategorien","contractCategories"), + ("PDF-Vorlagen", "pdfTemplates"), + ("HTML-Templates", "appSettings"), +]: + print(f" {label}: {d.get(key, 0)}") +skipped = d.get("pdfTemplatesSkipped", 0) +if skipped: + print(f" (PDF-Vorlagen übersprungen: {skipped})") +warnings = d.get("warnings", []) or [] +if warnings: + print("Hinweise:") + for w in warnings: + print(f" - {w}") +'; then + exit 0 +else + exit 1 +fi