factory-defaults: CLI-Sync zwischen dev und prod
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) <noreply@anthropic.com>
This commit is contained in:
@@ -35,3 +35,7 @@ data/factory-defaults/*
|
|||||||
!data/factory-defaults/.gitkeep
|
!data/factory-defaults/.gitkeep
|
||||||
data/backups/*
|
data/backups/*
|
||||||
!data/backups/.gitkeep
|
!data/backups/.gitkeep
|
||||||
|
|
||||||
|
# Factory-Defaults-Drop-Box (Export-ZIPs zwischen dev/prod hin und her)
|
||||||
|
factory-exports/*
|
||||||
|
!factory-exports/.gitkeep
|
||||||
|
|||||||
@@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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**
|
- [x] **🚀 Auto-Seed: Werkseinstellungen beim Erst-Deploy**
|
||||||
- Inhalt von `backend/factory-defaults/` wird via Dockerfile als
|
- Inhalt von `backend/factory-defaults/` wird via Dockerfile als
|
||||||
`/app/factory-defaults-builtin/` ins Image gebrannt.
|
`/app/factory-defaults-builtin/` ins Image gebrannt.
|
||||||
|
|||||||
Executable
+64
@@ -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/^/ /'
|
||||||
Executable
+97
@@ -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 <pfad/zur/factory-defaults.zip>"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user