factory-import: --save-as-builtin Flag + README-Überarbeitung

Schließt die Lücke „nach Import landet die ZIP nicht im Image-Default":

  ./factory-import.sh --save-as-builtin
  → entpackt die ZIP nach erfolgreichem DB-Import zusätzlich in
    backend/factory-defaults/ (alter Inhalt vorher aufgeräumt, README.md
    und .gitkeep bleiben). Beim nächsten Image-Build sind die Defaults
    drin und seeden frische VMs automatisch.

README-Abschnitt „Factory-Defaults" komplett überarbeitet:
- Drei Transport-Pfade explizit erklärt (laufende DB / Drop-Box / Image)
- HTML-Standardtexte + AppSetting-Whitelist dokumentiert
- Auto-Seed-Verhalten + Berechtigungen aktualisiert
- Typische Workflows als End-zu-End-Sequenz inkl. scp-Sync

Live verifiziert: STALE_FILE.txt im backend/factory-defaults/ wurde beim
--save-as-builtin sauber entfernt, README.md blieb erhalten, Subfolder neu
befüllt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:04:02 +02:00
parent 4407bbfbb8
commit ab971618d5
3 changed files with 198 additions and 65 deletions
+141 -54
View File
@@ -1066,8 +1066,9 @@ Folgende Felder werden in Audit-Logs gefiltert:
## Factory-Defaults: Stammdaten-Kataloge teilen ## Factory-Defaults: Stammdaten-Kataloge teilen
Das **Factory-Defaults**-System erlaubt den Export und Import von Das **Factory-Defaults**-System erlaubt den Export und Import von
Stammdaten-Katalogen (Anbieter, Tarife, PDF-Vorlagen usw.) zwischen verschiedenen Stammdaten-Katalogen (Anbieter, Tarife, PDF-Auftragsvorlagen, HTML-Standardtexte)
OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** zu Datenbank-Backups: zwischen verschiedenen OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt**
zu Datenbank-Backups:
### Abgrenzung ### Abgrenzung
@@ -1075,64 +1076,117 @@ OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** zu Datenbank-Backup
|---|---|---| |---|---|---|
| Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Kategorien | ✅ | ✅ | | Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Kategorien | ✅ | ✅ |
| PDF-Auftragsvorlagen (inkl. Dateien + Feldzuordnungen) | ✅ | ✅ | | PDF-Auftragsvorlagen (inkl. Dateien + Feldzuordnungen) | ✅ | ✅ |
| HTML-Standardtexte: Datenschutzerklärung, Impressum, Vollmacht-Vorlage, Website-Datenschutz | ✅ | ✅ |
| **Kundendaten, Verträge, Dokumente** | ❌ | ✅ | | **Kundendaten, Verträge, Dokumente** | ❌ | ✅ |
| **Emails, SMTP-/IMAP-Zugangsdaten** | ❌ | ✅ | | **Emails, SMTP-/IMAP-Zugangsdaten** | ❌ | ✅ |
| **System-Einstellungen, Datenschutzerklärungen, Impressum** | ❌ | ✅ | | **Secrets, JWT, Encryption-Keys, User-Accounts** | ❌ | ✅ |
| Zwischen verschiedenen Installationen teilbar | ✅ | ❌ (zu firmen-spezifisch) | | Zwischen verschiedenen Installationen teilbar | ✅ | ❌ (zu firmen-spezifisch) |
> **Kurz:** Factory-Defaults = reine Kataloge, Backup = alles. > **Kurz:** Factory-Defaults = generische Stammdaten + rechtliche Standardtexte,
> Backup = die komplette Instanz.
### Export (Installation A → ZIP) ### Drei Wege, eine ZIP zu transportieren
Es gibt drei Pfade, je nachdem wo die ZIP gerade liegen soll:
| Wo | Pfad | Wann |
|---|---|---|
| **Laufende DB einer Instanz** | UI-Upload oder `./factory-import.sh` | Bestehende Live-Instanz updaten |
| **Drop-Box im Repo** (`factory-exports/`) | `./factory-export.sh` legt ab, `./factory-import.sh` liest | Transfer zwischen dev und prod via `scp` |
| **Werkseinstellung im Image** (`backend/factory-defaults/`) | `./factory-import.sh --save-as-builtin` oder manuell entpacken | Neue VMs sollen die Defaults beim allerersten Start mitbringen |
Alle drei sind unabhängig, **alle drei zusammen** decken den typischen Workflow ab.
### Export
**Variante A UI:**
1. **Einstellungen** → **Factory-Defaults** öffnen 1. **Einstellungen** → **Factory-Defaults** öffnen
2. Übersicht prüfen (Anzahl pro Kategorie) 2. Button **„Factory-Defaults exportieren"** klicken
3. Button **„Factory-Defaults exportieren"** klicken 3. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen
4. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen
**Variante B CLI (für scp-Transfers):**
```bash
./factory-export.sh # → factory-exports/factory-defaults-…zip
OPENCRM_URL=https://crm.prod.example.de \
OPENCRM_EMAIL=admin@example.de ./factory-export.sh # gegen Prod-Instanz
```
Ohne `OPENCRM_PASSWORD` wird das Passwort interaktiv abgefragt. Der Zielordner
`factory-exports/` ist gitignored die ZIPs landen also nicht ins Repo.
**ZIP-Struktur:** **ZIP-Struktur:**
``` ```
factory-defaults-2026-04-23.zip factory-defaults-2026-05-07-1949.zip
├── manifest.json # Version + Datum + Counts ├── manifest.json # Version + Datum + Counts
├── providers/ ├── providers/providers.json
│ └── providers.json # Anbieter inkl. zugehöriger Tarife
├── contract-meta/ ├── contract-meta/
│ ├── cancellation-periods.json # Kündigungsfristen (Code + Beschreibung) │ ├── cancellation-periods.json
│ ├── contract-durations.json # Laufzeiten (Code + Beschreibung) │ ├── contract-durations.json
│ └── contract-categories.json # Kategorien (Strom, Gas, DSL, ...) │ └── contract-categories.json
── pdf-templates/ ── pdf-templates/
├── pdf-templates.json # Vorlagen-Metadaten + Feldzuordnungen ├── pdf-templates.json
└── *.pdf # Die eigentlichen PDF-Dateien └── *.pdf # Die eigentlichen PDF-Dateien
└── app-settings/
└── app-settings.json # HTML-Templates (Whitelist-only)
``` ```
Die ZIP kann an andere Installationen weitergegeben werden ### Import
(Partner, Test-System, neue Installation).
### Import (ZIP → Installation B) **Variante A UI:**
1. **Einstellungen** → **Factory-Defaults**
2. Bereich **Import** → **„ZIP hochladen"** → Datei wählen
3. Erfolgs-Box zeigt Counts pro Kategorie
1. ZIP herunterladen bzw. erhalten **Variante B CLI:**
2. Inhalt nach `backend/factory-defaults/` entpacken (Unterordnerstruktur beibehalten) ```bash
3. Im Backend-Verzeichnis ausführen: ./factory-import.sh # nimmt jüngste ZIP aus factory-exports/
```bash ./factory-import.sh ./factory-exports/foo.zip # explizite ZIP
npm run seed:defaults ./factory-import.sh --save-as-builtin # zusätzlich ins Image-Default
``` ./factory-import.sh --save-as-builtin ./foo.zip # entpacken (siehe unten)
```
Konfigurierbar per ENV: `OPENCRM_URL`, `OPENCRM_EMAIL`, `OPENCRM_PASSWORD`.
**Variante C Container-Bare-Metal (für Migration / mehrere ZIPs zusammenführen):**
```bash
# Inhalt der ZIP nach backend/factory-defaults/ entpacken (Unterordner beibehalten)
cd backend && npm run seed:defaults
```
**Beispiel-Output:** **Beispiel-Output:**
``` ```
📦 Factory-Defaults werden eingespielt... ✓ Anbieter: 10
✓ Tarife: 4
✓ Anbieter: 7, Tarife: 12 ✓ Kündigungsfristen: 18
✓ Kündigungsfristen: 5 ✓ Laufzeiten: 18
✓ Laufzeiten: 4 ✓ Vertragskategorien: 8
✓ Vertragskategorien: 8 ✓ PDF-Vorlagen: 2
✓ PDF-Vorlagen: 3 ✓ HTML-Templates: 2
✅ Factory-Defaults erfolgreich eingespielt.
``` ```
### Mehrere ZIPs kombinieren ### `--save-as-builtin`: ZIP zur Werkseinstellung machen
Du kannst mehrere Exporte in `backend/factory-defaults/` übereinanderlegen Mit `--save-as-builtin` entpackt `factory-import.sh` die ZIP nach **erfolgreichem
JSON-Dateien werden automatisch gemerged: DB-Import** zusätzlich in `backend/factory-defaults/`. Beim nächsten
`docker-compose up --build` landen die Defaults im Image. Frisch hochgezogene
VMs bringen sie dann beim ersten Start automatisch mit (Auto-Seed-Pfad im
Container-Entrypoint).
```bash
# typischer Sync prod → dev → Image-Default
ssh prod './factory-export.sh'
scp prod:opencrm/factory-exports/factory-defaults-*.zip factory-exports/
./factory-import.sh --save-as-builtin
docker-compose up -d --build # neuer Build, neue VMs starten direkt mit Defaults
```
Der Inhalt von `backend/factory-defaults/` wird beim `--save-as-builtin` vorher
geleert (außer `README.md` und `.gitkeep`), damit nichts Veraltetes liegen
bleibt.
### Mehrere ZIPs kombinieren (CLI-only, Variante C)
`backend/factory-defaults/` darf mehrere `*.json` pro Unterordner haben
`npm run seed:defaults` merged sie automatisch:
``` ```
backend/factory-defaults/ backend/factory-defaults/
@@ -1142,40 +1196,73 @@ backend/factory-defaults/
eigene.json # 5 eigene Anbieter eigene.json # 5 eigene Anbieter
``` ```
Das Import-Script liest **alle** `*.json` im jeweiligen Unterordner und merged per Bei gleichem Unique-Key gewinnt der zuletzt gelesene Eintrag. Der UI-/HTTP-Import
unique Key (letzter Eintrag gewinnt). Duplikate sind also unproblematisch. nimmt nur eine ZIP entgegen für Merges nutze `npm run seed:defaults`.
### Idempotenz ### Idempotenz
Das Script nutzt ausschließlich Prisma `upsert`: Alle Pfade nutzen Prisma `upsert`:
- **Neue Einträge** werden angelegt - **Neue Einträge** werden angelegt
- **Bestehende Einträge** (per unique Key: `name`, `code`) werden aktualisiert - **Bestehende Einträge** (per unique Key: `name` / `code` / `key`) werden aktualisiert
- Nichts wird gelöscht - Nichts wird gelöscht
Du kannst `npm run seed:defaults` also beliebig oft ausführen, ohne Datenverlust Du kannst Imports also beliebig oft hintereinander ausführen, ohne Datenverlust
oder Duplikate. oder Duplikate.
### PDF-Dateien beim Import ### PDF-Dateien
Beim Import werden PDF-Vorlagen aus `factory-defaults/pdf-templates/*.pdf` nach Beim Import werden PDF-Vorlagen aus dem ZIP nach `uploads/pdf-templates/`
`uploads/pdf-templates/` kopiert und die Pfade in der DB entsprechend gesetzt. kopiert (mit eindeutigem Suffix) und die `templatePath`-Spalte entsprechend
Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch die neue gesetzt. Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch
ersetzt. die neue ersetzt.
### AppSettings-Whitelist
Beim Import werden nur die Keys mit AppSetting-Schreibzugriff gewährt, die auch
exportiert werden aktuell:
- `privacyPolicyHtml`
- `imprintHtml`
- `authorizationTemplateHtml`
- `websitePrivacyPolicyHtml`
Andere Keys (SMTP, JWT, etc.) werden mit einer Warnung ignoriert. Whitelist ist
in [`backend/src/services/factoryDefaults.service.ts`](backend/src/services/factoryDefaults.service.ts)
zentral gepflegt.
### Auto-Seed beim Erst-Deploy
Bei einer **frischen** Installation (leere DB) spielt der Container-Entrypoint
nach dem Prisma-Seed automatisch das Built-in-Verzeichnis ein:
```
[entrypoint] DB ist leer (User-Count=0) Auto-Seed wird ausgeführt
[entrypoint] Spiele eingebaute Factory-Defaults ein…
✓ Anbieter: 10, Tarife: 4
```
Bei bestehenden Installs passiert das **nicht** nur frische DBs.
### Berechtigungen ### Berechtigungen
| Aktion | Berechtigung | | Aktion | Berechtigung |
|--------|--------------| |--------|--------------|
| Factory-Defaults Vorschau | `settings:read` | | Factory-Defaults Vorschau | `settings:read` |
| Factory-Defaults Export | `settings:update` | | Factory-Defaults Export (UI/CLI) | `settings:update` |
| Factory-Defaults Import (CLI) | Server-Zugang (SSH/Shell) | | Factory-Defaults Import (UI/CLI) | `settings:update` |
| Werkseinstellungen ändern (`--save-as-builtin` / `npm run seed:defaults`) | Server-Zugang (SSH/Shell) |
### Typischer Einsatzzweck ### Typische Einsatzzwecke
- **Neue Installation aufsetzen**: Eine Kollegen-ZIP importieren und sofort mit - **Neue VM aufsetzen**: ZIP einmalig nach `backend/factory-defaults/` entpacken
gepflegtem Anbieter- und Vorlagenkatalog loslegen (oder per `--save-as-builtin`), dann `docker-compose up --build` die
Werkseinstellungen sind beim ersten Start automatisch drin.
- **Prod-Stand zurück nach dev synchronisieren**: `./factory-export.sh` auf prod,
`scp` ins dev, `./factory-import.sh --save-as-builtin` lokal damit ist
sowohl die dev-DB aktuell als auch der nächste Image-Build.
- **Vorlagen-Paket teilen**: Eine ZIP mit nur PDF-Vorlagen weitergeben - **Vorlagen-Paket teilen**: Eine ZIP mit nur PDF-Vorlagen weitergeben
(die anderen Ordner einfach aus der ZIP entfernen vor dem Entpacken) (andere Ordner aus der ZIP entfernen vor dem Entpacken).
- **Anbieter-Paket teilen**: ZIP mit nur `providers/` weitergeben - **Anbieter-Paket teilen**: ZIP mit nur `providers/` weitergeben
- **Versionskontrolle**: Die entpackten JSON-Dateien unter Versionskontrolle - **Versionskontrolle**: Die entpackten JSON-Dateien unter Versionskontrolle
stellen (außerhalb von `backend/factory-defaults/`, da der Ordner gitignored ist) stellen (außerhalb von `backend/factory-defaults/`, da der Ordner gitignored ist)
+7 -4
View File
@@ -97,16 +97,19 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ Erledigt
- [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod)** - [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod ↔ Image)**
- `./factory-export.sh` zieht eine ZIP per API in `factory-exports/` - `./factory-export.sh` zieht eine ZIP per API in `factory-exports/`
(gitignored Drop-Box). (gitignored Drop-Box).
- `./factory-import.sh [zip]` lädt die ZIP per API in eine andere Instanz - `./factory-import.sh [zip]` lädt die ZIP per API in eine andere Instanz
ohne Argument wählt es die jüngste ZIP automatisch. ohne Argument wählt es die jüngste ZIP automatisch.
- `./factory-import.sh --save-as-builtin` entpackt die ZIP zusätzlich nach
`backend/factory-defaults/` (vorher aufgeräumt). Damit landet sie beim
nächsten `docker-compose up --build` als Werkseinstellung im Image und
seedet frische DBs automatisch.
- Konfigurierbar per Env: `OPENCRM_URL`, `OPENCRM_EMAIL`, - Konfigurierbar per Env: `OPENCRM_URL`, `OPENCRM_EMAIL`,
`OPENCRM_PASSWORD` (sonst interaktive Abfrage). `OPENCRM_PASSWORD` (sonst interaktive Abfrage).
- Use-Case: Anbieter/Tarife auf prod erweitert? `./factory-export.sh` auf - README-Abschnitt „Factory-Defaults: Stammdaten-Kataloge teilen"
prod, `scp` ins dev, `./factory-import.sh` lokal fertig. Geht in komplett überarbeitet (drei Transport-Pfade, Auto-Seed, Whitelist).
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
+50 -7
View File
@@ -3,9 +3,13 @@
# Idempotent (upserts pro Kategorie, nichts wird gelöscht). # Idempotent (upserts pro Kategorie, nichts wird gelöscht).
# #
# Aufruf: # Aufruf:
# ./factory-import.sh ./factory-exports/factory-defaults-2026-05-07-1923.zip # ./factory-import.sh # jüngste ZIP aus factory-exports/
# ./factory-import.sh # ohne Argument: nimmt automatisch # ./factory-import.sh ./factory-exports/foo.zip # explizite ZIP
# # die jüngste ZIP aus factory-exports/ # ./factory-import.sh --save-as-builtin # nach Import auch ins
# ./factory-import.sh --save-as-builtin ./foo.zip # backend/factory-defaults/
# # entpacken → nächster
# # Image-Build hat sie
# # als Werkseinstellung
# #
# ENV (wie factory-export.sh): # ENV (wie factory-export.sh):
# OPENCRM_URL (default http://localhost:3010) # OPENCRM_URL (default http://localhost:3010)
@@ -20,8 +24,29 @@ PASSWORD="${OPENCRM_PASSWORD:-}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXPORT_DIR="$REPO_ROOT/factory-exports" EXPORT_DIR="$REPO_ROOT/factory-exports"
BUILTIN_DIR="$REPO_ROOT/backend/factory-defaults"
# Argumente parsen: erlaubt sind --save-as-builtin und 0/1 ZIP-Pfade in
# beliebiger Reihenfolge.
SAVE_AS_BUILTIN=false
ZIP_PATH=""
for arg in "$@"; do
case "$arg" in
--save-as-builtin) SAVE_AS_BUILTIN=true ;;
-h|--help)
sed -n '2,16p' "$0" | sed 's/^# \?//'
exit 0
;;
--*) echo "✗ Unbekanntes Flag: $arg"; exit 2 ;;
*)
if [ -n "$ZIP_PATH" ]; then
echo "✗ Mehrere ZIP-Pfade angegeben (nur einer erlaubt)"; exit 2
fi
ZIP_PATH="$arg"
;;
esac
done
ZIP_PATH="${1:-}"
if [ -z "$ZIP_PATH" ]; then if [ -z "$ZIP_PATH" ]; then
# Jüngste ZIP automatisch wählen # Jüngste ZIP automatisch wählen
ZIP_PATH="$(ls -1t "$EXPORT_DIR"/*.zip 2>/dev/null | head -1 || true)" ZIP_PATH="$(ls -1t "$EXPORT_DIR"/*.zip 2>/dev/null | head -1 || true)"
@@ -64,7 +89,7 @@ RESPONSE="$(curl -sS -X POST "$URL/api/factory-defaults/import" \
-F "zip=@$ZIP_PATH")" -F "zip=@$ZIP_PATH")"
# Hübsch ausgeben + auf success prüfen # Hübsch ausgeben + auf success prüfen
if printf '%s' "$RESPONSE" | python3 -c ' if ! printf '%s' "$RESPONSE" | python3 -c '
import json, sys import json, sys
r = json.load(sys.stdin) r = json.load(sys.stdin)
if not r.get("success"): if not r.get("success"):
@@ -91,7 +116,25 @@ if warnings:
for w in warnings: for w in warnings:
print(f" - {w}") print(f" - {w}")
'; then '; then
exit 0
else
exit 1 exit 1
fi fi
# --save-as-builtin: ZIP zusätzlich in backend/factory-defaults/ entpacken,
# damit der nächste Image-Build sie als Werkseinstellung mitnimmt.
# Vorher räumen wir auf (außer README.md + .gitkeep), damit nichts Veraltetes
# liegen bleibt.
if [ "$SAVE_AS_BUILTIN" = "true" ]; then
echo
echo "→ --save-as-builtin: aktualisiere $BUILTIN_DIR/"
if [ ! -d "$BUILTIN_DIR" ]; then
mkdir -p "$BUILTIN_DIR"
fi
# Aufräumen: alles außer README.md und .gitkeep löschen
find "$BUILTIN_DIR" -mindepth 1 \
\! -name 'README.md' \! -name '.gitkeep' \
-delete
# ZIP entpacken (manifest.json kommt mit, ist aber harmlos)
unzip -q -o "$ZIP_PATH" -d "$BUILTIN_DIR"
echo "✓ Werkseinstellungen aktualisiert. Beim nächsten 'docker-compose up"
echo " --build' landen sie im Image und seeden frische DBs automatisch."
fi