Compare commits

...
2 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.7 4407bbfbb8 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>
2026-05-07 19:51:19 +02:00
duffyduckandClaude Opus 4.7 365c7994d5 factory-defaults: builtin-Werkseinstellungen beim Auto-Seed einspielen
Neue VMs sollen direkt mit den im Repo abgelegten Stammdaten +
Auftragsvorlagen + HTML-Templates hochkommen, ohne dass man jedes Mal
manuell ein ZIP hochlädt.

- Dockerfile: kopiert backend/factory-defaults nach
  /app/factory-defaults-builtin und backend/scripts nach /app/scripts
- seed-factory-defaults.ts: ROOT-Pfad über FACTORY_DEFAULTS_DIR überschreibbar
- entrypoint.sh: nach erfolgreichem Auto-Seed läuft `tsx
  scripts/seed-factory-defaults.ts` mit FACTORY_DEFAULTS_DIR auf den
  builtin-Pfad. Trigger NUR bei frischer DB (RAN_SEED=true), bestehende
  Installs werden nie nachträglich überschrieben.

`backend/factory-defaults/*` bleibt gitignored – Inhalte legt jeder
Operator-User selbst lokal ab (z.B. via Export-ZIP entpacken), sie landen
beim nächsten Container-Build im Image.

Live verifiziert: frischer Container mit RUN_SEED=true zieht 10 Anbieter,
4 Tarife, 18 Kündigungsfristen, 18 Laufzeiten, 8 Kategorien, 2 PDF-Vorlagen
und 2 HTML-Templates ein; PDFs landen mit eindeutigem Suffix in uploads/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:41:16 +02:00
8 changed files with 213 additions and 3 deletions
+4
View File
@@ -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
+6
View File
@@ -50,6 +50,12 @@ COPY backend/prisma ./prisma
# Frontend-Build ins public/-Verzeichnis (wird in production-Mode statisch ausgeliefert)
COPY --from=frontend-builder /build/frontend/dist ./public
# Eingebaute Werkseinstellungen ins Image: bei Erstinstallation (leerer DB) zieht
# der Entrypoint sie via tsx scripts/seed-factory-defaults.ts ein. Liegt in einem
# eigenen Pfad `factory-defaults/` selbst kann über Bind-Mount überlagert werden.
COPY backend/factory-defaults /app/factory-defaults-builtin
COPY backend/scripts /app/scripts
# Daten-Verzeichnisse (werden via Bind-Mount überlagert; hier nur als Fallback)
RUN mkdir -p uploads factory-defaults prisma/backups
+15 -2
View File
@@ -99,15 +99,28 @@ USER_COUNT=$(node -e "
.catch(() => { process.stdout.write('-1'); process.exit(0); });
" 2>/dev/null)
RAN_SEED=false
if [ "${RUN_SEED:-false}" = "true" ]; then
echo "[entrypoint] RUN_SEED=true seede DB (Force)"
npx prisma db seed || echo "[entrypoint] Seed fehlgeschlagen oder schon gelaufen ignoriert"
if npx prisma db seed; then RAN_SEED=true; else echo "[entrypoint] Seed fehlgeschlagen oder schon gelaufen ignoriert"; fi
elif [ "$USER_COUNT" = "0" ]; then
echo "[entrypoint] DB ist leer (User-Count=0) Auto-Seed wird ausgeführt"
npx prisma db seed || echo "[entrypoint] Auto-Seed fehlgeschlagen ignoriert"
if npx prisma db seed; then RAN_SEED=true; else echo "[entrypoint] Auto-Seed fehlgeschlagen ignoriert"; fi
else
echo "[entrypoint] DB enthält $USER_COUNT User kein Seed nötig"
fi
# Eingebaute Factory-Defaults nach Erstinstallation einspielen.
# Das ist die Werkseinstellung für neue VMs: PDF-Vorlagen, Anbieter, Tarife,
# HTML-Templates alles aus /app/factory-defaults-builtin/. Erfolgt nur wenn
# der Auto-Seed gerade lief (= frische DB), sonst werden Updates auf
# bestehenden Installationen nicht ungewollt überschrieben.
if [ "$RAN_SEED" = "true" ] && [ -d /app/factory-defaults-builtin ] \
&& [ -n "$(ls -A /app/factory-defaults-builtin 2>/dev/null | grep -v -E '^(README\.md|\.gitkeep)$')" ]; then
echo "[entrypoint] Spiele eingebaute Factory-Defaults ein…"
FACTORY_DEFAULTS_DIR=/app/factory-defaults-builtin npx tsx scripts/seed-factory-defaults.ts \
|| echo "[entrypoint] Factory-Defaults-Seed fehlgeschlagen ignoriert"
fi
echo "[entrypoint] Starte Backend…"
exec "$@"
+5 -1
View File
@@ -15,7 +15,11 @@ import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const ROOT = path.join(process.cwd(), 'factory-defaults');
// ROOT kann via FACTORY_DEFAULTS_DIR überschrieben werden (Container-Bootstrap
// mit eingebauten Defaults aus dem Image).
const ROOT = process.env.FACTORY_DEFAULTS_DIR
? path.resolve(process.env.FACTORY_DEFAULTS_DIR)
: path.join(process.cwd(), 'factory-defaults');
const UPLOADS_ROOT = path.join(process.cwd(), 'uploads');
const PDF_UPLOAD_DIR = path.join(UPLOADS_ROOT, 'pdf-templates');
+22
View File
@@ -97,6 +97,28 @@ 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.
- Entrypoint spielt sie nach erfolgreichem Auto-Seed (frische DB) automatisch
via `tsx scripts/seed-factory-defaults.ts` ein steuerbar über
`FACTORY_DEFAULTS_DIR`.
- Damit bringen neue VMs sofort Anbieter, Tarife, PDF-Auftragsvorlagen +
Datenschutzerklärung/Impressum mit, ohne manuelles UI-/CLI-Import.
- Bestehende Installs werden NIE überschrieben (Trigger nur wenn der
Auto-Seed im selben Start-Lauf gelaufen ist).
- [x] **📦 Factory-Defaults: HTML-Templates + Import via UI**
- Datenschutzerklärung, Impressum, Vollmacht-Vorlage und Website-Datenschutz
werden jetzt mit ins Factory-Defaults-ZIP gepackt (`app-settings/`-Ordner,
+64
View File
@@ -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/^/ /'
View File
+97
View File
@@ -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