Initial commit
This commit is contained in:
6
Docker Backend/.dockerignore
Normal file
6
Docker Backend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules;
|
||||
npm - debug.log.env.git.gitignore;
|
||||
README.md;
|
||||
test - scriptalizer.js;
|
||||
cache;
|
||||
output;
|
||||
29
Docker Backend/.env.example
Normal file
29
Docker Backend/.env.example
Normal file
@@ -0,0 +1,29 @@
|
||||
# Environment Configuration Example
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Node Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# Scriptalizer API Configuration
|
||||
SCRIPTALIZER_LICENSE_KEY=your-license-key-here
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
|
||||
# Preview Configuration
|
||||
# No batch size limit - frontend can send unlimited letters
|
||||
# Backend automatically splits into 25-letter batches for Scriptalizer API
|
||||
# No cache expiration
|
||||
# No rate limiting
|
||||
|
||||
# Server Configuration
|
||||
PORT=4000
|
||||
|
||||
# API Authentication (optional)
|
||||
API_TOKEN=your-api-token-here
|
||||
|
||||
# PayPal Configuration (for B2C payments)
|
||||
# Get credentials from PayPal Developer Dashboard:
|
||||
# https://developer.paypal.com/dashboard/applications/
|
||||
PAYPAL_CLIENT_ID=your-paypal-client-id
|
||||
PAYPAL_CLIENT_SECRET=your-paypal-client-secret
|
||||
# Options: 'sandbox' or 'live'
|
||||
PAYPAL_ENVIRONMENT=sandbox
|
||||
7
Docker Backend/.gitignore
vendored
Normal file
7
Docker Backend/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.env
|
||||
cache/
|
||||
output/
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
*.log
|
||||
423
Docker Backend/DEPLOYMENT.md
Normal file
423
Docker Backend/DEPLOYMENT.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# Deployment Anleitung - Skrift Backend
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Anleitung zeigt, wie du das Skrift Backend auf deinen vServer deployst.
|
||||
|
||||
## Voraussetzungen auf dem Server
|
||||
|
||||
- Ubuntu/Debian Linux Server
|
||||
- Docker und Docker Compose installiert
|
||||
- Nginx Proxy Manager läuft bereits
|
||||
- SSH-Zugang zum Server
|
||||
|
||||
## Option 1: Docker Image über Docker Hub (EMPFOHLEN)
|
||||
|
||||
### Schritt 1: Docker Image erstellen und pushen
|
||||
|
||||
Auf deinem lokalen Rechner:
|
||||
|
||||
```bash
|
||||
# 1. Ins Backend-Verzeichnis wechseln
|
||||
cd "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend"
|
||||
|
||||
# 2. Docker Login (benötigt Docker Hub Account)
|
||||
docker login
|
||||
|
||||
# 3. Image bauen (ersetze 'deinusername' mit deinem Docker Hub Username)
|
||||
docker build -t deinusername/skrift-backend:latest .
|
||||
|
||||
# 4. Image zu Docker Hub pushen
|
||||
docker push deinusername/skrift-backend:latest
|
||||
```
|
||||
|
||||
### Schritt 2: Auf dem Server deployen
|
||||
|
||||
Per SSH auf dem Server:
|
||||
|
||||
```bash
|
||||
# 1. Verzeichnis erstellen
|
||||
mkdir -p /opt/skrift-backend
|
||||
cd /opt/skrift-backend
|
||||
|
||||
# 2. docker-compose.yml erstellen (siehe unten)
|
||||
nano docker-compose.yml
|
||||
|
||||
# 3. .env Datei erstellen
|
||||
nano .env
|
||||
|
||||
# 4. Fonts-Ordner erstellen und Fonts hochladen
|
||||
mkdir fonts
|
||||
# Fonts per SCP hochladen (siehe Schritt 3)
|
||||
|
||||
# 5. Output-Verzeichnis erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# 6. Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# 7. Logs prüfen
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Schritt 3: Fonts per SCP hochladen
|
||||
|
||||
Auf deinem lokalen Rechner:
|
||||
|
||||
```bash
|
||||
# Fonts zum Server kopieren
|
||||
scp "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend\fonts\*.svg" \
|
||||
root@dein-server.de:/opt/skrift-backend/fonts/
|
||||
```
|
||||
|
||||
### docker-compose.yml für Server
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
skrift-backend:
|
||||
image: deinusername/skrift-backend:latest # Dein Docker Hub Image
|
||||
container_name: skrift-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4000:4000" # Oder anderer Port
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=4000
|
||||
- SCRIPTALIZER_LICENSE_KEY=${SCRIPTALIZER_LICENSE_KEY}
|
||||
- SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
- BATCH_SIZE=30
|
||||
- CACHE_LIFETIME_HOURS=2
|
||||
- RATE_LIMIT_PER_MINUTE=2
|
||||
volumes:
|
||||
- ./fonts:/app/fonts:ro
|
||||
- skrift-cache:/app/cache
|
||||
- /var/skrift-output:/app/output
|
||||
networks:
|
||||
- skrift-network
|
||||
|
||||
volumes:
|
||||
skrift-cache:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
skrift-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### .env Datei für Server
|
||||
|
||||
```bash
|
||||
# Scriptalizer API
|
||||
SCRIPTALIZER_LICENSE_KEY=f9918b40-d11c-11f0-b558-0800200c9a66
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
|
||||
# Preview Settings
|
||||
BATCH_SIZE=30
|
||||
CACHE_LIFETIME_HOURS=2
|
||||
RATE_LIMIT_PER_MINUTE=2
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
## Option 2: Direkt vom Server bauen
|
||||
|
||||
### Schritt 1: Code auf Server übertragen
|
||||
|
||||
```bash
|
||||
# Auf lokalem Rechner: Komplettes Backend-Verzeichnis kopieren
|
||||
scp -r "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend" \
|
||||
root@dein-server.de:/opt/skrift-backend/
|
||||
|
||||
# Oder mit rsync (besser):
|
||||
rsync -avz --exclude 'node_modules' --exclude 'output' --exclude 'cache' \
|
||||
"E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend/" \
|
||||
root@dein-server.de:/opt/skrift-backend/
|
||||
```
|
||||
|
||||
### Schritt 2: Auf Server bauen und starten
|
||||
|
||||
```bash
|
||||
# Per SSH auf dem Server
|
||||
cd /opt/skrift-backend
|
||||
|
||||
# Output-Verzeichnis erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# Container bauen und starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Logs prüfen
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## Option 3: Mit GitHub/GitLab (BESTE PRAXIS für Produktion)
|
||||
|
||||
### Schritt 1: Repository erstellen
|
||||
|
||||
```bash
|
||||
# Auf lokalem Rechner
|
||||
cd "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend"
|
||||
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
|
||||
# GitHub/GitLab Repository erstellen, dann:
|
||||
git remote add origin https://github.com/deinusername/skrift-backend.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### Schritt 2: Auf Server clonen und deployen
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
cd /opt
|
||||
git clone https://github.com/deinusername/skrift-backend.git
|
||||
cd skrift-backend
|
||||
|
||||
# .env erstellen (nicht in Git!)
|
||||
nano .env
|
||||
|
||||
# Output-Verzeichnis erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# Starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Bei Updates einfach:
|
||||
git pull
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Nginx Proxy Manager Konfiguration
|
||||
|
||||
1. Im Nginx Proxy Manager eine neue "Proxy Host" erstellen:
|
||||
- **Domain Names**: `backend.deine-domain.de` (oder Subdomain deiner Wahl)
|
||||
- **Scheme**: `http`
|
||||
- **Forward Hostname/IP**: `skrift-backend` (Container-Name)
|
||||
- **Forward Port**: `4000`
|
||||
- **Cache Assets**: aktivieren
|
||||
- **Block Common Exploits**: aktivieren
|
||||
- **Websockets Support**: deaktivieren
|
||||
|
||||
2. SSL-Zertifikat:
|
||||
- Tab "SSL" öffnen
|
||||
- "Request a new SSL Certificate" auswählen
|
||||
- "Force SSL" aktivieren
|
||||
- Let's Encrypt Email eingeben
|
||||
|
||||
3. Optional - Custom Nginx Configuration:
|
||||
```nginx
|
||||
# Größere Request Body Size für große Texte
|
||||
client_max_body_size 10M;
|
||||
|
||||
# Timeouts für lange Scriptalizer-Calls
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
```
|
||||
|
||||
## Nützliche Docker-Befehle
|
||||
|
||||
```bash
|
||||
# Container Status prüfen
|
||||
docker-compose ps
|
||||
|
||||
# Logs anzeigen
|
||||
docker-compose logs -f
|
||||
|
||||
# Container neu starten
|
||||
docker-compose restart
|
||||
|
||||
# Container stoppen
|
||||
docker-compose down
|
||||
|
||||
# Container stoppen und Volumes löschen
|
||||
docker-compose down -v
|
||||
|
||||
# In Container-Shell gehen
|
||||
docker exec -it skrift-backend sh
|
||||
|
||||
# Image neu bauen
|
||||
docker-compose build --no-cache
|
||||
|
||||
# Alte Images aufräumen
|
||||
docker image prune -a
|
||||
```
|
||||
|
||||
## Gesundheits-Check
|
||||
|
||||
Nach dem Deployment:
|
||||
|
||||
```bash
|
||||
# Lokal vom Server
|
||||
curl http://localhost:4000/health
|
||||
|
||||
# Über Domain (nach Nginx Proxy Setup)
|
||||
curl https://backend.deine-domain.de/health
|
||||
|
||||
# Sollte zurückgeben:
|
||||
# {"status":"ok","timestamp":"2026-01-03T..."}
|
||||
```
|
||||
|
||||
## WordPress Integration
|
||||
|
||||
In deinem WordPress Plugin (Frontend) die Backend-URL konfigurieren:
|
||||
|
||||
```javascript
|
||||
// WordPress Plugin Einstellungen
|
||||
const BACKEND_URL = 'https://backend.deine-domain.de';
|
||||
|
||||
// API Calls
|
||||
fetch(`${BACKEND_URL}/api/preview/batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: 'session-uuid',
|
||||
letters: [...]
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## N8N Integration
|
||||
|
||||
N8N kann die generierten Dateien im `/var/skrift-output` Verzeichnis abholen:
|
||||
|
||||
```bash
|
||||
# N8N Workflow - File Trigger Node
|
||||
/var/skrift-output/*/order-metadata.json
|
||||
```
|
||||
|
||||
Oder per API:
|
||||
|
||||
```bash
|
||||
# Webhook in N8N, wenn Bestellung fertig ist
|
||||
POST https://n8n.deine-domain.de/webhook/order-complete
|
||||
{
|
||||
"orderNumber": "SK-2026-01-03-001",
|
||||
"path": "/var/skrift-output/SK-2026-01-03-001"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container startet nicht
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker-compose logs
|
||||
|
||||
# Typische Probleme:
|
||||
# - Fonts fehlen: Fonts-Ordner prüfen
|
||||
# - Port 4000 belegt: Port in docker-compose.yml ändern
|
||||
# - .env fehlt: .env Datei erstellen
|
||||
```
|
||||
|
||||
### Fonts werden nicht gefunden
|
||||
```bash
|
||||
# In Container prüfen
|
||||
docker exec -it skrift-backend ls -la /app/fonts
|
||||
|
||||
# Sollte zeigen:
|
||||
# tilda.svg
|
||||
# alva.svg
|
||||
# ellie.svg
|
||||
```
|
||||
|
||||
### API antwortet nicht
|
||||
```bash
|
||||
# Nginx Proxy Logs prüfen
|
||||
docker logs nginx-proxy-manager
|
||||
|
||||
# Backend Logs prüfen
|
||||
docker-compose logs skrift-backend
|
||||
|
||||
# Firewall prüfen
|
||||
sudo ufw status
|
||||
sudo ufw allow 4000/tcp # Falls direkt ohne Proxy
|
||||
```
|
||||
|
||||
### Durchgestrichene Wörter erscheinen
|
||||
```bash
|
||||
# SCRIPTALIZER_ERR_FREQUENCY in .env auf 0 setzen
|
||||
echo "SCRIPTALIZER_ERR_FREQUENCY=0" >> .env
|
||||
|
||||
# Container neu starten
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Einfaches Monitoring mit Healthcheck
|
||||
|
||||
```bash
|
||||
# Cronjob erstellen für Health-Check
|
||||
crontab -e
|
||||
|
||||
# Jede 5 Minuten prüfen
|
||||
*/5 * * * * curl -f http://localhost:4000/health || systemctl restart skrift-backend
|
||||
```
|
||||
|
||||
### Mit Uptime Kuma (empfohlen)
|
||||
|
||||
1. Uptime Kuma installieren (auch als Docker Container)
|
||||
2. HTTP(s) Monitor erstellen für `https://backend.deine-domain.de/health`
|
||||
3. Alert bei Ausfall per E-Mail/Telegram
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Fonts sichern (einmalig)
|
||||
tar -czf skrift-fonts-backup.tar.gz /opt/skrift-backend/fonts
|
||||
|
||||
# Output-Dateien sichern (täglich via Cronjob)
|
||||
tar -czf skrift-output-$(date +%Y%m%d).tar.gz /var/skrift-output
|
||||
|
||||
# Zu externem Speicher kopieren
|
||||
rsync -avz /var/skrift-output/ backup-server:/backups/skrift/
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
### Update über Docker Hub
|
||||
```bash
|
||||
# Neues Image bauen und pushen (lokal)
|
||||
docker build -t deinusername/skrift-backend:latest .
|
||||
docker push deinusername/skrift-backend:latest
|
||||
|
||||
# Auf Server updaten
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Update über Git
|
||||
```bash
|
||||
# Auf Server
|
||||
cd /opt/skrift-backend
|
||||
git pull
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- `.env` Datei NIEMALS in Git committen
|
||||
- Regelmäßig Updates: `docker-compose pull`
|
||||
- Nginx Proxy Manager für SSL/TLS
|
||||
- Firewall: Nur notwendige Ports öffnen
|
||||
- Rate Limiting ist bereits im Backend implementiert
|
||||
- Regelmäßige Backups der Output-Dateien
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
1. Logs prüfen: `docker-compose logs -f`
|
||||
2. Health-Endpoint testen: `curl http://localhost:4000/health`
|
||||
3. Container Status: `docker-compose ps`
|
||||
4. Ins Container-Shell: `docker exec -it skrift-backend sh`
|
||||
319
Docker Backend/DEPLOYMENT_EINFACH.md
Normal file
319
Docker Backend/DEPLOYMENT_EINFACH.md
Normal file
@@ -0,0 +1,319 @@
|
||||
# Einfaches Deployment OHNE Docker Hub
|
||||
|
||||
## Warum diese Methode?
|
||||
|
||||
- ✅ Kein Docker Hub Account nötig
|
||||
- ✅ Keine Gefahr, dass andere dein Image sehen
|
||||
- ✅ Schneller und einfacher
|
||||
- ✅ Perfekt für einen einzelnen Server
|
||||
|
||||
## Deployment-Prozess
|
||||
|
||||
### Schritt 1: Backend zum Server kopieren
|
||||
|
||||
**Option A: Mit SCP (Windows PowerShell oder Git Bash)**
|
||||
|
||||
```bash
|
||||
# Temporäres Archiv erstellen (ohne unnötige Dateien)
|
||||
cd "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend"
|
||||
|
||||
# Zip erstellen
|
||||
tar -czf skrift-backend.tar.gz \
|
||||
--exclude='node_modules' \
|
||||
--exclude='output' \
|
||||
--exclude='cache' \
|
||||
--exclude='.git' \
|
||||
--exclude='*.md' \
|
||||
--exclude='bruno-tests' \
|
||||
--exclude='test-*.js' \
|
||||
--exclude='generate-*.js' \
|
||||
src/ fonts/ package.json package-lock.json Dockerfile docker-compose.yml .dockerignore
|
||||
|
||||
# Zum Server kopieren
|
||||
scp skrift-backend.tar.gz root@DEINE-SERVER-IP:/tmp/
|
||||
|
||||
# Auf Server entpacken
|
||||
ssh root@DEINE-SERVER-IP << 'ENDSSH'
|
||||
mkdir -p /opt/skrift-backend
|
||||
cd /opt/skrift-backend
|
||||
tar -xzf /tmp/skrift-backend.tar.gz
|
||||
rm /tmp/skrift-backend.tar.gz
|
||||
ENDSSH
|
||||
```
|
||||
|
||||
**Option B: Mit RSYNC (empfohlen, wenn verfügbar)**
|
||||
|
||||
```bash
|
||||
# Direkt synchronisieren (nur geänderte Dateien)
|
||||
rsync -avz --progress \
|
||||
--exclude='node_modules' \
|
||||
--exclude='output' \
|
||||
--exclude='cache' \
|
||||
--exclude='.git' \
|
||||
--exclude='*.md' \
|
||||
--exclude='bruno-tests' \
|
||||
--exclude='test-*.js' \
|
||||
--exclude='generate-*.js' \
|
||||
"/e/Dokumente/05_Skrift/Frontend_Backend_Konfigurator/Docker Backend/" \
|
||||
root@DEINE-SERVER-IP:/opt/skrift-backend/
|
||||
```
|
||||
|
||||
**Option C: Mit WinSCP (GUI)**
|
||||
|
||||
1. WinSCP herunterladen und installieren
|
||||
2. Verbindung zu deinem Server herstellen
|
||||
3. Verzeichnis erstellen: `/opt/skrift-backend`
|
||||
4. Diese Ordner hochladen:
|
||||
- `src/` (kompletter Ordner)
|
||||
- `fonts/` (kompletter Ordner)
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `Dockerfile`
|
||||
- `docker-compose.yml`
|
||||
- `.dockerignore`
|
||||
|
||||
### Schritt 2: Auf dem Server einrichten
|
||||
|
||||
Per SSH auf den Server:
|
||||
|
||||
```bash
|
||||
ssh root@DEINE-SERVER-IP
|
||||
|
||||
# Ins Backend-Verzeichnis
|
||||
cd /opt/skrift-backend
|
||||
|
||||
# .env Datei erstellen (WICHTIG!)
|
||||
cat > .env << 'EOF'
|
||||
SCRIPTALIZER_LICENSE_KEY=f9918b40-d11c-11f0-b558-0800200c9a66
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
BATCH_SIZE=30
|
||||
CACHE_LIFETIME_HOURS=2
|
||||
RATE_LIMIT_PER_MINUTE=2
|
||||
NODE_ENV=production
|
||||
PORT=4000
|
||||
EOF
|
||||
|
||||
# Berechtigungen setzen
|
||||
chmod 600 .env
|
||||
|
||||
# Output-Verzeichnis für N8N erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# Docker Image bauen und Container starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Logs ansehen (Ctrl+C zum Beenden)
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Schritt 3: Testen
|
||||
|
||||
```bash
|
||||
# Health-Check auf dem Server
|
||||
curl http://localhost:4000/health
|
||||
|
||||
# Sollte zurückgeben:
|
||||
# {"status":"ok","timestamp":"2026-01-03T..."}
|
||||
```
|
||||
|
||||
### Schritt 4: Nginx Proxy Manager einrichten
|
||||
|
||||
1. Öffne Nginx Proxy Manager (z.B. `http://dein-server.de:81`)
|
||||
2. Login mit Admin-Credentials
|
||||
3. "Proxy Hosts" → "Add Proxy Host"
|
||||
|
||||
**Details Tab:**
|
||||
- Domain Names: `backend.deine-domain.de` (oder `skrift-backend.deine-domain.de`)
|
||||
- Scheme: `http`
|
||||
- Forward Hostname/IP: `skrift-backend` (der Docker Container Name!)
|
||||
- Forward Port: `4000`
|
||||
- Cache Assets: ✓ aktivieren
|
||||
- Block Common Exploits: ✓ aktivieren
|
||||
- Websockets Support: nicht aktivieren
|
||||
|
||||
**SSL Tab:**
|
||||
- SSL Certificate: "Request a new SSL Certificate"
|
||||
- Force SSL: ✓ aktivieren
|
||||
- Email Address: deine-email@domain.de
|
||||
- I Agree to the Terms: ✓ aktivieren
|
||||
|
||||
4. "Save" klicken
|
||||
|
||||
### Schritt 5: Finaler Test
|
||||
|
||||
Von deinem lokalen Rechner:
|
||||
|
||||
```bash
|
||||
curl https://backend.deine-domain.de/health
|
||||
```
|
||||
|
||||
## Updates durchführen
|
||||
|
||||
Wenn du Code-Änderungen gemacht hast:
|
||||
|
||||
```bash
|
||||
# 1. Lokal: Neue Version zum Server kopieren
|
||||
rsync -avz --progress \
|
||||
--exclude='node_modules' \
|
||||
--exclude='output' \
|
||||
--exclude='cache' \
|
||||
"/e/Dokumente/05_Skrift/Frontend_Backend_Konfigurator/Docker Backend/" \
|
||||
root@DEINE-SERVER-IP:/opt/skrift-backend/
|
||||
|
||||
# 2. Auf Server: Container neu bauen
|
||||
ssh root@DEINE-SERVER-IP "cd /opt/skrift-backend && docker-compose up -d --build"
|
||||
|
||||
# 3. Logs prüfen
|
||||
ssh root@DEINE-SERVER-IP "cd /opt/skrift-backend && docker-compose logs -f"
|
||||
```
|
||||
|
||||
## Automatisches Update-Script
|
||||
|
||||
Erstelle eine Datei `update.sh` auf deinem lokalen Rechner:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
SERVER="root@DEINE-SERVER-IP"
|
||||
REMOTE_PATH="/opt/skrift-backend"
|
||||
LOCAL_PATH="/e/Dokumente/05_Skrift/Frontend_Backend_Konfigurator/Docker Backend"
|
||||
|
||||
echo "Syncing files to server..."
|
||||
rsync -avz --progress \
|
||||
--exclude='node_modules' \
|
||||
--exclude='output' \
|
||||
--exclude='cache' \
|
||||
--exclude='.git' \
|
||||
"$LOCAL_PATH/" \
|
||||
"$SERVER:$REMOTE_PATH/"
|
||||
|
||||
echo "Rebuilding container on server..."
|
||||
ssh $SERVER "cd $REMOTE_PATH && docker-compose up -d --build"
|
||||
|
||||
echo "Done! Checking logs..."
|
||||
ssh $SERVER "cd $REMOTE_PATH && docker-compose logs --tail=50"
|
||||
```
|
||||
|
||||
Dann einfach ausführen:
|
||||
```bash
|
||||
bash update.sh
|
||||
```
|
||||
|
||||
## Vorteile dieser Methode
|
||||
|
||||
✅ **Privat**: Nur auf deinem Server, niemand sonst kann es sehen
|
||||
✅ **Einfach**: Keine Docker Hub Registrierung nötig
|
||||
✅ **Schnell**: Direkt auf dem Server gebaut
|
||||
✅ **Sicher**: Keine Credentials in der Cloud
|
||||
✅ **Flexibel**: Änderungen sofort deployen
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Verbindung fehlgeschlagen
|
||||
```bash
|
||||
# SSH-Verbindung testen
|
||||
ssh root@DEINE-SERVER-IP
|
||||
|
||||
# Falls Fehler: SSH-Key einrichten
|
||||
ssh-keygen -t rsa -b 4096
|
||||
ssh-copy-id root@DEINE-SERVER-IP
|
||||
```
|
||||
|
||||
### Container startet nicht
|
||||
```bash
|
||||
# Logs prüfen
|
||||
ssh root@DEINE-SERVER-IP "cd /opt/skrift-backend && docker-compose logs"
|
||||
|
||||
# Häufige Probleme:
|
||||
# - .env fehlt → Schritt 2 wiederholen
|
||||
# - Fonts fehlen → fonts/ Ordner hochladen
|
||||
# - Port 4000 belegt → docker-compose.yml anpassen
|
||||
```
|
||||
|
||||
### Nginx Proxy erreicht Container nicht
|
||||
```bash
|
||||
# Prüfen ob Container im richtigen Netzwerk ist
|
||||
ssh root@DEINE-SERVER-IP "docker network ls"
|
||||
ssh root@DEINE-SERVER-IP "docker network inspect skrift-network"
|
||||
|
||||
# Container Name prüfen
|
||||
ssh root@DEINE-SERVER-IP "docker ps | grep skrift"
|
||||
|
||||
# In Nginx Proxy Manager: Forward Hostname = "skrift-backend" (Container-Name)
|
||||
```
|
||||
|
||||
### Build dauert zu lange
|
||||
```bash
|
||||
# Cache löschen und neu bauen
|
||||
ssh root@DEINE-SERVER-IP "cd /opt/skrift-backend && docker-compose build --no-cache"
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- `.env` Datei wird **nur** auf dem Server erstellt (nicht hochgeladen)
|
||||
- `.dockerignore` verhindert Upload von sensiblen Dateien
|
||||
- Nur notwendige Ports werden geöffnet (4000 nur intern)
|
||||
- Nginx Proxy Manager mit SSL-Verschlüsselung
|
||||
- Rate Limiting ist bereits im Backend implementiert
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
# 1. Code-Backup
|
||||
tar -czf /root/skrift-backend-backup-$(date +%Y%m%d).tar.gz /opt/skrift-backend
|
||||
|
||||
# 2. Output-Dateien Backup
|
||||
tar -czf /root/skrift-output-backup-$(date +%Y%m%d).tar.gz /var/skrift-output
|
||||
|
||||
# 3. Zu lokalem Rechner herunterladen
|
||||
scp root@DEINE-SERVER-IP:/root/skrift-*-backup-*.tar.gz ./backups/
|
||||
```
|
||||
|
||||
## Kompletter Workflow - Schritt für Schritt
|
||||
|
||||
```bash
|
||||
# === AUF LOKALEM RECHNER ===
|
||||
|
||||
# 1. Ins Verzeichnis wechseln
|
||||
cd "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend"
|
||||
|
||||
# 2. Zum Server kopieren
|
||||
rsync -avz --exclude='node_modules' --exclude='output' --exclude='cache' ./ root@SERVER:/opt/skrift-backend/
|
||||
|
||||
|
||||
# === AUF DEM SERVER (per SSH) ===
|
||||
|
||||
# 3. Verbinden
|
||||
ssh root@SERVER
|
||||
|
||||
# 4. Setup
|
||||
cd /opt/skrift-backend
|
||||
cat > .env << 'EOF'
|
||||
SCRIPTALIZER_LICENSE_KEY=f9918b40-d11c-11f0-b558-0800200c9a66
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
BATCH_SIZE=30
|
||||
NODE_ENV=production
|
||||
EOF
|
||||
|
||||
mkdir -p /var/skrift-output
|
||||
docker-compose up -d --build
|
||||
|
||||
# 5. Testen
|
||||
curl http://localhost:4000/health
|
||||
|
||||
|
||||
# === IN NGINX PROXY MANAGER (Browser) ===
|
||||
|
||||
# 6. Proxy Host erstellen
|
||||
# Domain: backend.deine-domain.de
|
||||
# Forward to: skrift-backend:4000
|
||||
# SSL: Let's Encrypt aktivieren
|
||||
|
||||
|
||||
# === FERTIG! ===
|
||||
|
||||
# Von überall testen:
|
||||
curl https://backend.deine-domain.de/health
|
||||
```
|
||||
31
Docker Backend/Dockerfile
Normal file
31
Docker Backend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install dependencies for better-sqlite3 if needed later
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies (npm install instead of ci for flexibility)
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/cache/previews \
|
||||
&& mkdir -p /app/output \
|
||||
&& mkdir -p /app/fonts
|
||||
|
||||
# Expose port
|
||||
EXPOSE 4000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
|
||||
|
||||
# Start application
|
||||
CMD ["node", "src/server.js"]
|
||||
198
Docker Backend/HANDSCHRIFT_VARIATIONEN.md
Normal file
198
Docker Backend/HANDSCHRIFT_VARIATIONEN.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Handschrift-Variationen - Konfiguration
|
||||
|
||||
## Aktuelle Einstellungen
|
||||
|
||||
### 1. Scriptalizer Error Frequency
|
||||
```
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
```
|
||||
- **Bedeutung:** Keine durchgestrichenen Wörter oder Tippfehler
|
||||
- **Ergebnis:** Saubere, fehlerfreie Handschrift
|
||||
|
||||
---
|
||||
|
||||
### 2. Wortabstands-Variation: **15%**
|
||||
|
||||
**Implementierung:** `src/lib/svg-font-engine.js`
|
||||
|
||||
```javascript
|
||||
// Leerzeichen mit 15% Variation
|
||||
const baseSpace = fontSizePx * 0.4;
|
||||
const variation = baseSpace * 0.15 * (Math.sin(wordIndex * 2.5) * 0.5 + 0.5);
|
||||
const spacePx = baseSpace + variation;
|
||||
```
|
||||
|
||||
**Effekt:**
|
||||
- Basis-Wortabstand: 40% der Schriftgröße (≈10.4 px bei 26px Font)
|
||||
- Variation: ±1.56 px (15% von 10.4px)
|
||||
- Natürliche, ungleichmäßige Abstände zwischen Wörtern
|
||||
|
||||
---
|
||||
|
||||
### 3. Wort-Rotation (Schräglage): **±5%** (≈±2.5°)
|
||||
|
||||
**Implementierung:** `src/lib/svg-font-engine.js`
|
||||
|
||||
```javascript
|
||||
// Rotation zwischen -2.5° und +2.5° (±0.044 radians)
|
||||
const rotationVariation = 0.044 * (Math.sin(wordIndex * 3.7) * 0.5 + 0.5) - 0.022;
|
||||
wordRotation = rotationVariation;
|
||||
|
||||
// Matrix-Transformation mit Rotation
|
||||
const cosR = Math.cos(wordRotation);
|
||||
const sinR = Math.sin(wordRotation);
|
||||
const transform = `matrix(${scale * cosR},${scale * sinR},${-scale * sinR},${-scale * cosR},${x},${baselineY})`;
|
||||
```
|
||||
|
||||
**Effekt:**
|
||||
- Jedes Wort bekommt eine individuelle, leichte Schräglage
|
||||
- Rotation: -2.5° bis +2.5° (±0.044 Radians)
|
||||
- Macht die Handschrift natürlicher und lebendiger
|
||||
- Jedes Wort ist leicht unterschiedlich geneigt
|
||||
|
||||
---
|
||||
|
||||
## Variation-Pattern
|
||||
|
||||
### Sinuswellen-Basis
|
||||
Beide Variationen verwenden Sinuswellen für natürliche, nicht-gleichförmige Muster:
|
||||
|
||||
```javascript
|
||||
Math.sin(wordIndex * 2.5) // Wortabstand (langsame Oszillation)
|
||||
Math.sin(wordIndex * 3.7) // Rotation (schnellere Oszillation)
|
||||
```
|
||||
|
||||
**Vorteil:**
|
||||
- Keine zufälligen Sprünge
|
||||
- Sanfte, natürliche Übergänge
|
||||
- Reproduzierbar (gleicher Text → gleiche Variation)
|
||||
- Kein Seed-Management nötig
|
||||
|
||||
---
|
||||
|
||||
## Visuelle Beispiele
|
||||
|
||||
### Ohne Variation (alt):
|
||||
```
|
||||
Hallo Max Mustermann aus Berlin
|
||||
```
|
||||
Alle Wörter perfekt gerade, gleiche Abstände
|
||||
|
||||
### Mit Variation (neu):
|
||||
```
|
||||
Hallo Max Mustermann aus Berlin
|
||||
↗ → ↘ → ↗
|
||||
```
|
||||
- Unterschiedliche Wortabstände (15% Variation)
|
||||
- Leichte Schräglage pro Wort (±2.5°)
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### SVG Matrix-Transformation
|
||||
|
||||
**Original (ohne Rotation):**
|
||||
```xml
|
||||
<path transform="matrix(0.00846,0,0,-0.00846,75.6,120.5)" />
|
||||
```
|
||||
|
||||
**Neu (mit Rotation):**
|
||||
```xml
|
||||
<path transform="matrix(0.00845,0.00004,-0.00004,-0.00845,75.6,120.5)" />
|
||||
```
|
||||
|
||||
Die kleinen Werte in Position 2 und 3 der Matrix erzeugen die Rotation:
|
||||
- Position 2 (b): `scale * sin(rotation)` ≈ 0.00004
|
||||
- Position 3 (c): `-scale * sin(rotation)` ≈ -0.00004
|
||||
|
||||
---
|
||||
|
||||
## Anpassung der Variationen
|
||||
|
||||
### Wortabstand ändern
|
||||
|
||||
**Datei:** `src/lib/svg-font-engine.js` (Zeile 187-189)
|
||||
|
||||
```javascript
|
||||
// Aktuell: 15%
|
||||
const variation = baseSpace * 0.15 * (Math.sin(wordIndex * 2.5) * 0.5 + 0.5);
|
||||
|
||||
// Mehr Variation (z.B. 25%):
|
||||
const variation = baseSpace * 0.25 * (Math.sin(wordIndex * 2.5) * 0.5 + 0.5);
|
||||
|
||||
// Weniger Variation (z.B. 8%):
|
||||
const variation = baseSpace * 0.08 * (Math.sin(wordIndex * 2.5) * 0.5 + 0.5);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rotation (Schräglage) ändern
|
||||
|
||||
**Datei:** `src/lib/svg-font-engine.js` (Zeile 200-202)
|
||||
|
||||
```javascript
|
||||
// Aktuell: ±2.5° (0.044 radians)
|
||||
const rotationVariation = 0.044 * (Math.sin(wordIndex * 3.7) * 0.5 + 0.5) - 0.022;
|
||||
|
||||
// Stärker schräg (z.B. ±5°):
|
||||
const rotationVariation = 0.087 * (Math.sin(wordIndex * 3.7) * 0.5 + 0.5) - 0.0435;
|
||||
|
||||
// Weniger schräg (z.B. ±1°):
|
||||
const rotationVariation = 0.0175 * (Math.sin(wordIndex * 3.7) * 0.5 + 0.5) - 0.00875;
|
||||
|
||||
// Keine Rotation (deaktivieren):
|
||||
const rotationVariation = 0;
|
||||
```
|
||||
|
||||
**Umrechnung Grad → Radians:**
|
||||
```
|
||||
Radians = Grad × (π / 180)
|
||||
±1° = ±0.0175 rad
|
||||
±2.5° = ±0.044 rad
|
||||
±5° = ±0.087 rad
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test-Beispiel
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/api/preview/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"sessionId": "variation-demo",
|
||||
"letters": [{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Dies ist ein Test mit natürlicher Handschrift-Variation.\n\nDie Wörter haben unterschiedliche Abstände und eine leichte Schräglage.\n\nDas macht das Schriftbild authentischer!",
|
||||
"placeholders": {}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
**Impact:**
|
||||
- Minimaler Overhead durch sin/cos Berechnungen
|
||||
- Pro Zeichen: ~4 zusätzliche Operationen
|
||||
- Bei 2000 Zeichen: ~0.5ms zusätzliche Verarbeitungszeit
|
||||
- Vernachlässigbar im Vergleich zur Scriptalizer API (1-3 Sekunden)
|
||||
|
||||
---
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
✅ **Alle SVG-Viewer** unterstützen Matrix-Transformationen
|
||||
✅ **Alle Browser** (Chrome, Firefox, Safari, Edge)
|
||||
✅ **Plotter-Software** verarbeitet transformierte Pfade korrekt
|
||||
✅ **Keine Änderung der Pfad-Daten** (nur Transformation)
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.1.0
|
||||
**Letzte Änderung:** 2026-01-02
|
||||
**Status:** Produktionsreif ✅
|
||||
169
Docker Backend/QUICKSTART.md
Normal file
169
Docker Backend/QUICKSTART.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Quick Start - Deployment in 5 Minuten
|
||||
|
||||
## Schnellste Methode: Per SCP auf Server kopieren
|
||||
|
||||
### 1. Server-Voraussetzungen prüfen
|
||||
|
||||
SSH auf deinen Server und prüfe:
|
||||
|
||||
```bash
|
||||
# Docker installiert?
|
||||
docker --version
|
||||
|
||||
# Docker Compose installiert?
|
||||
docker-compose --version
|
||||
|
||||
# Falls nicht installiert:
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
```
|
||||
|
||||
### 2. Backend auf Server kopieren
|
||||
|
||||
Auf deinem **lokalen Windows-Rechner** (Git Bash oder WSL):
|
||||
|
||||
```bash
|
||||
# Ins Backend-Verzeichnis
|
||||
cd "/e/Dokumente/05_Skrift/Frontend_Backend_Konfigurator/Docker Backend"
|
||||
|
||||
# Zum Server kopieren (ersetze USER und SERVER)
|
||||
rsync -avz \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'output' \
|
||||
--exclude 'cache' \
|
||||
--exclude '.git' \
|
||||
./ root@dein-server.de:/opt/skrift-backend/
|
||||
```
|
||||
|
||||
**Oder mit SCP (wenn rsync nicht verfügbar):**
|
||||
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
scp -r "E:\Dokumente\05_Skrift\Frontend_Backend_Konfigurator\Docker Backend\*" root@dein-server.de:/opt/skrift-backend/
|
||||
```
|
||||
|
||||
### 3. Auf dem Server starten
|
||||
|
||||
SSH auf den Server:
|
||||
|
||||
```bash
|
||||
ssh root@dein-server.de
|
||||
|
||||
# Ins Backend-Verzeichnis
|
||||
cd /opt/skrift-backend
|
||||
|
||||
# .env Datei erstellen
|
||||
cat > .env << 'EOF'
|
||||
SCRIPTALIZER_LICENSE_KEY=f9918b40-d11c-11f0-b558-0800200c9a66
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
BATCH_SIZE=30
|
||||
CACHE_LIFETIME_HOURS=2
|
||||
RATE_LIMIT_PER_MINUTE=2
|
||||
NODE_ENV=production
|
||||
EOF
|
||||
|
||||
# Output-Verzeichnis für N8N erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# Container bauen und starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### 4. Testen
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
curl http://localhost:4000/health
|
||||
|
||||
# Sollte antworten mit:
|
||||
# {"status":"ok","timestamp":"..."}
|
||||
```
|
||||
|
||||
### 5. Nginx Proxy Manager einrichten
|
||||
|
||||
1. Nginx Proxy Manager öffnen (z.B. http://dein-server.de:81)
|
||||
2. "Proxy Hosts" → "Add Proxy Host"
|
||||
3. Konfiguration:
|
||||
- **Domain Names**: `backend.deine-domain.de`
|
||||
- **Scheme**: `http`
|
||||
- **Forward Hostname/IP**: `skrift-backend`
|
||||
- **Forward Port**: `4000`
|
||||
- **Cache Assets**: ✓
|
||||
- **Block Common Exploits**: ✓
|
||||
|
||||
4. Tab "SSL":
|
||||
- **SSL Certificate**: "Request a new SSL Certificate"
|
||||
- **Force SSL**: ✓
|
||||
- **Email**: deine@email.de
|
||||
|
||||
5. Speichern
|
||||
|
||||
### 6. Finaler Test
|
||||
|
||||
```bash
|
||||
# Von deinem lokalen Rechner
|
||||
curl https://backend.deine-domain.de/health
|
||||
```
|
||||
|
||||
## FERTIG!
|
||||
|
||||
Dein Backend läuft jetzt auf: `https://backend.deine-domain.de`
|
||||
|
||||
### WordPress Integration
|
||||
|
||||
In deinem WordPress Plugin die Backend-URL eintragen:
|
||||
|
||||
```php
|
||||
define('SKRIFT_BACKEND_URL', 'https://backend.deine-domain.de');
|
||||
```
|
||||
|
||||
## Wichtige Befehle
|
||||
|
||||
```bash
|
||||
# Container Status
|
||||
docker-compose ps
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f
|
||||
|
||||
# Container neu starten
|
||||
docker-compose restart
|
||||
|
||||
# Container stoppen
|
||||
docker-compose down
|
||||
|
||||
# Update nach Code-Änderungen
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Problemlösung
|
||||
|
||||
### Container startet nicht
|
||||
```bash
|
||||
docker-compose logs
|
||||
# Häufig: Fonts fehlen oder .env nicht korrekt
|
||||
```
|
||||
|
||||
### Port 4000 schon belegt
|
||||
```bash
|
||||
# In docker-compose.yml ändern:
|
||||
ports:
|
||||
- "4001:4000" # Anderen externen Port verwenden
|
||||
```
|
||||
|
||||
### Keine Verbindung von außen
|
||||
```bash
|
||||
# Firewall prüfen
|
||||
sudo ufw status
|
||||
sudo ufw allow 4000/tcp
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- Siehe [DEPLOYMENT.md](./DEPLOYMENT.md) für Details
|
||||
- Siehe [README.md](./README.md) für API-Dokumentation
|
||||
- N8N Workflow einrichten für automatische Plotter-Übertragung
|
||||
461
Docker Backend/README.md
Normal file
461
Docker Backend/README.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Skrift Backend - Handwritten Document Generator
|
||||
|
||||
Docker-basiertes Backend für die Generierung von handschriftlichen Dokumenten (Briefe, Postkarten, Umschläge) mit SVG-Output.
|
||||
|
||||
## Features
|
||||
|
||||
- **Preview-System**: Batch-Generierung von Vorschauen mit Caching (30 Briefe pro Batch)
|
||||
- **Scriptalizer Integration**: Nutzt externe API für natürliche Handschrift-Variationen
|
||||
- **SVG-Generierung**: Eigene Font-Engine für hochqualitative SVG-Ausgabe
|
||||
- **Multi-Format Support**: A4, A6 (Hoch-/Querformat), C6, DIN Lang Umschläge
|
||||
- **Platzhalter-System**: Automatische Ersetzung von `[[Platzhalter]]` mit CSV-Export
|
||||
- **Rate Limiting**: Schutz vor API-Spam (konfigurierbar)
|
||||
- **Docker-Ready**: Vollständig containerisiert mit docker-compose
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Konfiguration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# .env bearbeiten und Scriptalizer License Key eintragen
|
||||
```
|
||||
|
||||
### 2. Lokal testen (ohne Docker)
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Server läuft auf: `http://localhost:4000`
|
||||
|
||||
### 3. Mit Docker deployen
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### Health Check
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2026-01-15T12:00:00Z",
|
||||
"uptime": 12345,
|
||||
"scriptalizer": "configured",
|
||||
"storage": {
|
||||
"cache": true,
|
||||
"output": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Preview Batch Generierung
|
||||
|
||||
```
|
||||
POST /api/preview/batch
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "uuid-abc-123",
|
||||
"batchIndex": 0,
|
||||
"forceRegenerate": false,
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"text": "Hallo [[Vorname]], dein Code ist [[Gutscheincode]]...",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Strasse": "Hauptstr. 1",
|
||||
"PLZ": "10115",
|
||||
"Ort": "Berlin",
|
||||
"Gutscheincode": "SAVE20"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Strasse": "Hauptstr. 1",
|
||||
"PLZ": "10115",
|
||||
"Ort": "Berlin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "uuid-abc-123",
|
||||
"files": [
|
||||
{
|
||||
"type": "letter",
|
||||
"index": 0,
|
||||
"url": "/api/preview/uuid-abc-123/letter_000.svg"
|
||||
},
|
||||
{
|
||||
"type": "envelope",
|
||||
"index": 0,
|
||||
"url": "/api/preview/uuid-abc-123/envelope_000.svg"
|
||||
}
|
||||
],
|
||||
"csvUrl": "/api/preview/uuid-abc-123/platzhalter.csv",
|
||||
"expiresAt": "2026-01-15T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Rate Limit:** 2 Requests/Minute pro sessionId
|
||||
|
||||
---
|
||||
|
||||
### Preview-Datei abrufen
|
||||
|
||||
```
|
||||
GET /api/preview/:sessionId/:filename
|
||||
```
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
GET /api/preview/uuid-abc-123/letter_000.svg
|
||||
```
|
||||
|
||||
**Response:** SVG-Datei (Content-Type: image/svg+xml)
|
||||
|
||||
---
|
||||
|
||||
### Bestellung finalisieren (aus Cache)
|
||||
|
||||
```
|
||||
POST /api/order/finalize
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "uuid-abc-123",
|
||||
"orderNumber": "SK-2026-01-15-001"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-15-001",
|
||||
"outputPath": "/app/output/SK-2026-01-15-001",
|
||||
"files": {
|
||||
"letters": 100,
|
||||
"envelopes": 100,
|
||||
"csv": "platzhalter.csv"
|
||||
},
|
||||
"timestamp": "2026-01-15T12:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bestellung neu generieren (ohne Cache)
|
||||
|
||||
```
|
||||
POST /api/order/generate
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-15-002",
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [...],
|
||||
"envelopes": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Gleich wie `/api/order/finalize`
|
||||
|
||||
---
|
||||
|
||||
## Formate
|
||||
|
||||
### Schriftstücke (Letters)
|
||||
- `a4` - A4 Hochformat (210 × 297 mm)
|
||||
- `a6p` - A6 Hochformat (105 × 148 mm)
|
||||
- `a6l` - A6 Querformat (148 × 105 mm)
|
||||
|
||||
### Umschläge (Envelopes)
|
||||
- `c6` - C6 Umschlag (162 × 114 mm)
|
||||
- `din_lang` - DIN Lang Umschlag (220 × 110 mm)
|
||||
|
||||
### Fonts
|
||||
- `tilda` - PremiumUltra79
|
||||
- `alva` - PremiumUltra23
|
||||
- `ellie` - PremiumUltra39
|
||||
|
||||
---
|
||||
|
||||
## Umschlag-Typen
|
||||
|
||||
### Empfänger-Adresse (type: "recipient")
|
||||
Adresse wird **unten links** positioniert (kein Sichtfenster).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Strasse": "Hauptstr. 1",
|
||||
"PLZ": "10115",
|
||||
"Ort": "Berlin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Individueller Text (type: "custom")
|
||||
Text wird **mittig zentriert** positioniert. Max. 150 Zeichen.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "custom",
|
||||
"data": {
|
||||
"customText": "Für meine großartige Freundin Caro"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
```
|
||||
/app/
|
||||
├── cache/
|
||||
│ └── previews/
|
||||
│ └── {sessionId}/
|
||||
│ ├── letter_000.svg
|
||||
│ ├── envelope_000.svg
|
||||
│ ├── platzhalter.csv
|
||||
│ └── metadata.json
|
||||
│
|
||||
├── output/
|
||||
│ └── {orderNumber}/
|
||||
│ ├── schriftstuecke/
|
||||
│ │ ├── brief_000.svg
|
||||
│ │ └── ...
|
||||
│ ├── umschlaege/
|
||||
│ │ ├── umschlag_000.svg
|
||||
│ │ └── ...
|
||||
│ ├── platzhalter.csv
|
||||
│ └── order-metadata.json
|
||||
│
|
||||
└── fonts/
|
||||
├── tilda.svg
|
||||
├── alva.svg
|
||||
└── ellie.svg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
```bash
|
||||
# Node Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# Scriptalizer API
|
||||
SCRIPTALIZER_LICENSE_KEY=your-key-here
|
||||
SCRIPTALIZER_ERR_FREQUENCY=10
|
||||
|
||||
# Preview System
|
||||
BATCH_SIZE=30
|
||||
CACHE_LIFETIME_HOURS=2
|
||||
RATE_LIMIT_PER_MINUTE=2
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
CORS_ORIGIN=*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Auf Server (mit Docker)
|
||||
|
||||
```bash
|
||||
# .env Datei erstellen mit production values
|
||||
docker-compose up -d
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f
|
||||
|
||||
# Stoppen
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Nginx Proxy Manager Setup
|
||||
|
||||
1. Proxy Host erstellen
|
||||
2. Domain: `api.skrift.de` (oder deine Domain)
|
||||
3. Forward Hostname/IP: `localhost`
|
||||
4. Forward Port: `4000`
|
||||
5. SSL Zertifikat über NPM erstellen
|
||||
|
||||
---
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Lokales Testen
|
||||
|
||||
```bash
|
||||
npm run dev # Mit nodemon
|
||||
```
|
||||
|
||||
### Scriptalizer Separator Test
|
||||
|
||||
```bash
|
||||
npm run test:separator
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# Docker logs
|
||||
docker-compose logs -f skrift-backend
|
||||
|
||||
# Lokale logs
|
||||
# Output in console
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration mit N8N
|
||||
|
||||
N8N kann direkt auf den `/app/output/{orderNumber}/` Ordner zugreifen:
|
||||
|
||||
```javascript
|
||||
// N8N Workflow (Beispiel)
|
||||
const fs = require('fs');
|
||||
const orderPath = '/var/skrift-output/SK-2026-01-15-001';
|
||||
|
||||
// Lese alle SVGs
|
||||
const letters = fs.readdirSync(`${orderPath}/schriftstuecke`);
|
||||
|
||||
// Sende an Plotter
|
||||
for (const file of letters) {
|
||||
await sendToPlotter(`${orderPath}/schriftstuecke/${file}`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
- `200` - Success
|
||||
- `400` - Bad Request (z.B. ungültige Parameter)
|
||||
- `404` - Not Found (z.B. Session nicht gefunden)
|
||||
- `410` - Gone (z.B. Cache abgelaufen)
|
||||
- `429` - Too Many Requests (Rate Limit)
|
||||
- `500` - Internal Server Error
|
||||
- `503` - Service Unavailable (z.B. Scriptalizer down)
|
||||
|
||||
### Typische Fehler
|
||||
|
||||
**Rate Limit überschritten:**
|
||||
```json
|
||||
{
|
||||
"error": "Zu viele Vorschau-Anfragen. Bitte warten Sie.",
|
||||
"retryAfter": 45,
|
||||
"message": "Limit: 2 Anfragen pro Minute"
|
||||
}
|
||||
```
|
||||
|
||||
**Scriptalizer Fehler:**
|
||||
```json
|
||||
{
|
||||
"error": "Scriptalizer request failed: timeout"
|
||||
}
|
||||
```
|
||||
|
||||
**Cache abgelaufen:**
|
||||
```json
|
||||
{
|
||||
"error": "Preview-Session abgelaufen. Bitte neu generieren."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limits
|
||||
|
||||
- **Scriptalizer API**: 10.000 Calls/Tag
|
||||
- **Batch Size**: 30 Briefe pro Request
|
||||
- **Input Size**: 48KB pro Scriptalizer Call
|
||||
- **Rate Limit**: 2 Preview-Requests/Minute
|
||||
- **Cache Lifetime**: 2 Stunden
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Fonts nicht gefunden
|
||||
|
||||
```bash
|
||||
# Fonts kopieren
|
||||
cp /path/to/fonts/*.svg ./fonts/
|
||||
```
|
||||
|
||||
### Scriptalizer API Fehler
|
||||
|
||||
```bash
|
||||
# License Key prüfen
|
||||
cat .env | grep SCRIPTALIZER_LICENSE_KEY
|
||||
|
||||
# Test-Script ausführen
|
||||
npm run test:separator
|
||||
```
|
||||
|
||||
### Permissions Fehler
|
||||
|
||||
```bash
|
||||
# Cache/Output Ordner Permissions
|
||||
chmod -R 755 cache output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weitere Infos
|
||||
|
||||
- **Scriptalizer API**: [www.scriptalizer.co.uk](https://www.scriptalizer.co.uk)
|
||||
- **Support**: Siehe Issues in Repository
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: 2026-01-01
|
||||
331
Docker Backend/TILDA_API_BEISPIEL.md
Normal file
331
Docker Backend/TILDA_API_BEISPIEL.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Tilda Font API-Aufruf Beispiel
|
||||
|
||||
## Konfiguration
|
||||
- **Error Frequency:** 0 (keine durchgestrichenen Wörter)
|
||||
- **Wortabstand-Variation:** ±5% für natürlicheres Schriftbild
|
||||
- **Font:** Tilda (Scriptalizer: PremiumUltra79)
|
||||
|
||||
---
|
||||
|
||||
## 1. Preview-Generierung (empfohlen)
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
POST http://localhost:4000/api/preview/batch
|
||||
```
|
||||
|
||||
### Request Headers
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"sessionId": "meine-session-12345",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte Damen und Herren,\n\nhiermit möchten wir Ihnen mitteilen, dass Ihre Bestellung erfolgreich bearbeitet wurde.\n\nWir bedanken uns für Ihr Vertrauen und freuen uns auf eine weitere Zusammenarbeit.\n\nMit freundlichen Grüßen\nIhr Skrift-Team",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### cURL Beispiel
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/api/preview/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"sessionId": "meine-session-12345",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte Damen und Herren,\n\nhiermit möchten wir Ihnen mitteilen, dass Ihre Bestellung erfolgreich bearbeitet wurde.\n\nWir bedanken uns für Ihr Vertrauen und freuen uns auf eine weitere Zusammenarbeit.\n\nMit freundlichen Grüßen\nIhr Skrift-Team",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"sessionId": "meine-session-12345",
|
||||
"files": [
|
||||
{
|
||||
"index": 0,
|
||||
"filename": "letter_000.svg",
|
||||
"url": "/api/preview/meine-session-12345/letter_000.svg"
|
||||
}
|
||||
],
|
||||
"csvUrl": "/api/preview/meine-session-12345/placeholders.csv"
|
||||
}
|
||||
```
|
||||
|
||||
### Preview SVG abrufen
|
||||
```bash
|
||||
curl http://localhost:4000/api/preview/meine-session-12345/letter_000.svg -o preview.svg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Mit Platzhaltern
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"sessionId": "platzhalter-test",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nhiermit bestätigen wir Ihre Bestellung [[Bestellnummer]].\n\nIhr persönlicher Gutscheincode: [[Gutscheincode]]\nGültig bis: [[Ablaufdatum]]\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Bestellnummer": "SK-2026-001",
|
||||
"Gutscheincode": "SAVE20",
|
||||
"Ablaufdatum": "31.12.2026"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Mehrere Briefe (Batch)
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"sessionId": "batch-test",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nvielen Dank für Ihre Bestellung!",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nvielen Dank für Ihre Bestellung!",
|
||||
"placeholders": {
|
||||
"Vorname": "Anna",
|
||||
"Nachname": "Schmidt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nvielen Dank für Ihre Bestellung!",
|
||||
"placeholders": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Hinweis:** Bis zu 30 Briefe pro Batch möglich (konfigurierbar)
|
||||
|
||||
---
|
||||
|
||||
## 4. Bestellung finalisieren
|
||||
|
||||
Nach der Vorschau kann die Bestellung finalisiert werden:
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
POST http://localhost:4000/api/order/finalize
|
||||
```
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"sessionId": "meine-session-12345",
|
||||
"orderNumber": "SK-2026-01-02-001"
|
||||
}
|
||||
```
|
||||
|
||||
### cURL Beispiel
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/api/order/finalize \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"sessionId": "meine-session-12345",
|
||||
"orderNumber": "SK-2026-01-02-001"
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-001",
|
||||
"outputPath": "/app/output/SK-2026-01-02-001",
|
||||
"files": [
|
||||
"letter_000.svg",
|
||||
"placeholders.csv"
|
||||
],
|
||||
"timestamp": "2026-01-02T09:11:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Direkte Bestellungs-Generierung (ohne Preview)
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
POST http://localhost:4000/api/order/generate
|
||||
```
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-002",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte Damen und Herren,\n\nhiermit bestätigen wir den Eingang Ihrer Bestellung.\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### cURL Beispiel
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/api/order/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"orderNumber": "SK-2026-01-02-002",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte Damen und Herren,\n\nhiermit bestätigen wir den Eingang Ihrer Bestellung.\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verfügbare Formate
|
||||
|
||||
- **a4**: A4 Hochformat (210 × 297 mm)
|
||||
- **a6p**: A6 Hochformat (105 × 148 mm)
|
||||
- **a6l**: A6 Querformat (148 × 105 mm)
|
||||
|
||||
---
|
||||
|
||||
## Verfügbare Fonts
|
||||
|
||||
- **tilda**: Scriptalizer PremiumUltra79
|
||||
- **alva**: Scriptalizer PremiumUltra23
|
||||
- **ellie**: Scriptalizer PremiumUltra39
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
✅ **Keine Fehler:** Error Frequency = 0 (keine durchgestrichenen Wörter)
|
||||
✅ **Wortabstand-Variation:** ±5% natürliche Variation für realistischeres Schriftbild
|
||||
✅ **Platzhalter:** Automatische Ersetzung von [[Platzhalter]]
|
||||
✅ **CSV-Export:** Automatische Generierung der Platzhalter-Tabelle
|
||||
✅ **Batch-Verarbeitung:** Bis zu 30 Briefe pro Request
|
||||
✅ **Preview-Caching:** 2 Stunden Cache für schnellere Finalisierung
|
||||
✅ **Rate Limiting:** 2 Requests/Minute Schutz
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
Die generierten SVG-Dateien befinden sich in:
|
||||
- **Preview:** `/app/cache/previews/{sessionId}/`
|
||||
- **Finale Bestellung:** `/app/output/{orderNumber}/`
|
||||
|
||||
Struktur:
|
||||
```
|
||||
output/SK-2026-01-02-001/
|
||||
├── letter_000.svg
|
||||
├── letter_001.svg
|
||||
├── placeholders.csv
|
||||
└── order-metadata.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Beispiel Output
|
||||
|
||||
**letter_000.svg:**
|
||||
```xml
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 793.8 1122.66">
|
||||
<rect x="0" y="0" width="793.8" height="1122.66" fill="#FFFFFF" />
|
||||
<path d="M1549 1857q-55 73 -124.5 125..."
|
||||
transform="matrix(0.00846,0,0,-0.00846,75.6,120.5)"
|
||||
stroke="#000000"
|
||||
stroke-width="0.6"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<!-- ~2000 weitere Pfade für handgeschriebenen Text -->
|
||||
</svg>
|
||||
```
|
||||
|
||||
Dateigröße: ~100-120 KB pro A4-Seite
|
||||
|
||||
---
|
||||
|
||||
## WordPress Integration Beispiel
|
||||
|
||||
```javascript
|
||||
// Im WordPress Plugin
|
||||
const response = await fetch('https://api.skrift.de/api/preview/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId: generateUUID(),
|
||||
letters: [
|
||||
{
|
||||
index: 0,
|
||||
format: 'a4',
|
||||
font: 'tilda',
|
||||
text: brieftext,
|
||||
placeholders: platzhalterDaten
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
// Zeige Preview: data.files[0].url
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Stand:** 2026-01-02
|
||||
**Version:** 1.0.0
|
||||
21
Docker Backend/bruno-tests/1 Health Check.bru
Normal file
21
Docker Backend/bruno-tests/1 Health Check.bru
Normal file
@@ -0,0 +1,21 @@
|
||||
meta {
|
||||
name: 1 Health Check
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/health
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Status is 200", function() {
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
test("Scriptalizer is configured", function() {
|
||||
expect(res.body.scriptalizer).to.equal("configured");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
meta {
|
||||
name: 2 Preview Batch - Single Letter
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/preview/batch
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sessionId": "{{sessionId}}",
|
||||
"batchIndex": 0,
|
||||
"forceRegenerate": false,
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"text": "Hallo [[Vorname]] [[Nachname]],\n\ndein persönlicher Gutscheincode lautet: [[Gutscheincode]]\n\nEr ist gültig bis zum [[Ablaufdatum]].\n\nViele Grüße,\nDein Skrift-Team",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Gutscheincode": "SAVE20",
|
||||
"Ablaufdatum": "31.12.2026"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Status is 200", function() {
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
test("Returns session ID", function() {
|
||||
expect(res.body.sessionId).to.be.a("string");
|
||||
});
|
||||
|
||||
test("Returns files array", function() {
|
||||
expect(res.body.files).to.be.an("array");
|
||||
expect(res.body.files.length).to.be.greaterThan(0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
meta {
|
||||
name: 3 Preview Batch - With Envelope
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/preview/batch
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sessionId": "{{sessionId}}",
|
||||
"batchIndex": 0,
|
||||
"forceRegenerate": false,
|
||||
"config": {
|
||||
"font": "alva",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a6p",
|
||||
"text": "Liebe [[Vorname]],\n\nvielen Dank für deine Bestellung!\n\nHerzliche Grüße",
|
||||
"placeholders": {
|
||||
"Vorname": "Anna",
|
||||
"Nachname": "Schmidt",
|
||||
"Strasse": "Bahnhofstr. 5",
|
||||
"PLZ": "80331",
|
||||
"Ort": "München"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Anna",
|
||||
"Nachname": "Schmidt",
|
||||
"Strasse": "Bahnhofstr. 5",
|
||||
"PLZ": "80331",
|
||||
"Ort": "München"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Returns letter and envelope", function() {
|
||||
const files = res.body.files;
|
||||
const hasLetter = files.some(f => f.type === "letter");
|
||||
const hasEnvelope = files.some(f => f.type === "envelope");
|
||||
expect(hasLetter).to.be.true;
|
||||
expect(hasEnvelope).to.be.true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: 4 Preview Batch - Custom Envelope Text
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/preview/batch
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sessionId": "{{sessionId}}-custom",
|
||||
"batchIndex": 0,
|
||||
"config": {
|
||||
"font": "ellie",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a6l",
|
||||
"text": "Alles Gute zum Geburtstag!",
|
||||
"placeholders": {}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "din_lang",
|
||||
"type": "custom",
|
||||
"data": {
|
||||
"customText": "Für meine großartige Freundin Caro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Docker Backend/bruno-tests/5 Order Finalize.bru
Normal file
39
Docker Backend/bruno-tests/5 Order Finalize.bru
Normal file
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: 5 Order Finalize
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/order/finalize
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"sessionId": "{{sessionId}}",
|
||||
"orderNumber": "{{orderNumber}}"
|
||||
}
|
||||
}
|
||||
|
||||
docs {
|
||||
Finalisiert eine Bestellung aus dem Preview-Cache.
|
||||
|
||||
WICHTIG: Vorher muss ein Preview Batch generiert worden sein!
|
||||
Run "2 Preview Batch - Single Letter" first.
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Status is 200", function() {
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
test("Returns order number", function() {
|
||||
expect(res.body.orderNumber).to.equal(bru.getEnvVar("orderNumber"));
|
||||
});
|
||||
|
||||
test("Has output path", function() {
|
||||
expect(res.body.outputPath).to.be.a("string");
|
||||
});
|
||||
}
|
||||
96
Docker Backend/bruno-tests/6 Order Generate.bru
Normal file
96
Docker Backend/bruno-tests/6 Order Generate.bru
Normal file
@@ -0,0 +1,96 @@
|
||||
meta {
|
||||
name: 6 Order Generate
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/order/generate
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"orderNumber": "SK-2026-01-15-002",
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nhiermit bestätigen wir Ihre Bestellung.\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "a4",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nhiermit bestätigen wir Ihre Bestellung.\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {
|
||||
"Vorname": "Julia",
|
||||
"Nachname": "Weber",
|
||||
"Strasse": "Kastanienallee 7",
|
||||
"PLZ": "60311",
|
||||
"Ort": "Frankfurt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "c6",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Julia",
|
||||
"Nachname": "Weber",
|
||||
"Strasse": "Kastanienallee 7",
|
||||
"PLZ": "60311",
|
||||
"Ort": "Frankfurt"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
docs {
|
||||
Generiert eine Bestellung direkt (ohne Preview-Cache).
|
||||
|
||||
Use case: Retry nach Fehler oder manuelles Regenerieren.
|
||||
}
|
||||
|
||||
tests {
|
||||
test("Status is 200", function() {
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
|
||||
test("Generated 2 letters", function() {
|
||||
expect(res.body.files.letters).to.equal(2);
|
||||
});
|
||||
|
||||
test("Generated 2 envelopes", function() {
|
||||
expect(res.body.files.envelopes).to.equal(2);
|
||||
});
|
||||
|
||||
test("Has CSV", function() {
|
||||
expect(res.body.files.csv).to.be.a("string");
|
||||
});
|
||||
}
|
||||
5
Docker Backend/bruno-tests/bruno.json
Normal file
5
Docker Backend/bruno-tests/bruno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Skrift Backend API",
|
||||
"type": "collection"
|
||||
}
|
||||
5
Docker Backend/bruno-tests/environments/Local.bru
Normal file
5
Docker Backend/bruno-tests/environments/Local.bru
Normal file
@@ -0,0 +1,5 @@
|
||||
vars {
|
||||
baseUrl: http://localhost:4000
|
||||
sessionId: test-session-{{$timestamp}}
|
||||
orderNumber: SK-2026-01-15-001
|
||||
}
|
||||
252
Docker Backend/deploy.sh
Normal file
252
Docker Backend/deploy.sh
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Skrift Backend - Deployment Script für Server
|
||||
# Dieses Script automatisiert das Deployment auf dem Server
|
||||
|
||||
set -e # Exit bei Fehler
|
||||
|
||||
echo "========================================="
|
||||
echo "Skrift Backend - Deployment Script"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Konfiguration
|
||||
DEPLOY_USER="root"
|
||||
DEPLOY_HOST=""
|
||||
DEPLOY_PATH="/opt/skrift-backend"
|
||||
DOCKER_IMAGE_NAME="skrift-backend"
|
||||
DOCKER_HUB_USER=""
|
||||
|
||||
# Funktion: Hilfe anzeigen
|
||||
show_help() {
|
||||
echo "Verwendung: ./deploy.sh [OPTION]"
|
||||
echo ""
|
||||
echo "Optionen:"
|
||||
echo " local - Docker Image lokal bauen und testen"
|
||||
echo " push - Image zu Docker Hub pushen"
|
||||
echo " server-scp - Backend per SCP zum Server kopieren"
|
||||
echo " server-deploy - Auf Server deployen (via Docker Hub)"
|
||||
echo " server-ssh - SSH-Verbindung zum Server öffnen"
|
||||
echo " help - Diese Hilfe anzeigen"
|
||||
echo ""
|
||||
echo "Beispiele:"
|
||||
echo " ./deploy.sh local # Lokal testen"
|
||||
echo " ./deploy.sh push # Image hochladen"
|
||||
echo " ./deploy.sh server-deploy # Auf Server deployen"
|
||||
}
|
||||
|
||||
# Funktion: Konfiguration prüfen
|
||||
check_config() {
|
||||
if [ -z "$DEPLOY_HOST" ]; then
|
||||
echo "ERROR: DEPLOY_HOST ist nicht gesetzt!"
|
||||
echo "Bitte in diesem Script DEPLOY_HOST='dein-server.de' setzen"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" == "push" ] && [ -z "$DOCKER_HUB_USER" ]; then
|
||||
echo "ERROR: DOCKER_HUB_USER ist nicht gesetzt!"
|
||||
echo "Bitte in diesem Script DOCKER_HUB_USER='deinusername' setzen"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Funktion: Lokal bauen und testen
|
||||
build_local() {
|
||||
echo "Building Docker Image lokal..."
|
||||
docker build -t $DOCKER_IMAGE_NAME:latest .
|
||||
|
||||
echo ""
|
||||
echo "Image erfolgreich gebaut!"
|
||||
echo ""
|
||||
echo "Zum Testen:"
|
||||
echo " docker run -p 4000:4000 --env-file .env -v \$(pwd)/fonts:/app/fonts:ro $DOCKER_IMAGE_NAME:latest"
|
||||
echo ""
|
||||
echo "Dann im Browser: http://localhost:4000/health"
|
||||
}
|
||||
|
||||
# Funktion: Image zu Docker Hub pushen
|
||||
push_dockerhub() {
|
||||
check_config "push"
|
||||
|
||||
echo "Logging in to Docker Hub..."
|
||||
docker login
|
||||
|
||||
echo "Tagging image..."
|
||||
docker tag $DOCKER_IMAGE_NAME:latest $DOCKER_HUB_USER/$DOCKER_IMAGE_NAME:latest
|
||||
|
||||
echo "Pushing to Docker Hub..."
|
||||
docker push $DOCKER_HUB_USER/$DOCKER_IMAGE_NAME:latest
|
||||
|
||||
echo ""
|
||||
echo "Image erfolgreich gepusht: $DOCKER_HUB_USER/$DOCKER_IMAGE_NAME:latest"
|
||||
}
|
||||
|
||||
# Funktion: Backend per SCP zum Server kopieren
|
||||
deploy_scp() {
|
||||
check_config
|
||||
|
||||
echo "Kopiere Backend-Dateien zum Server..."
|
||||
|
||||
# Temporäres Verzeichnis für saubere Dateien erstellen
|
||||
echo "Erstelle sauberes Deployment-Package..."
|
||||
rm -rf ./deploy-temp
|
||||
mkdir -p ./deploy-temp
|
||||
|
||||
# Nur notwendige Dateien kopieren
|
||||
rsync -av \
|
||||
--exclude 'node_modules' \
|
||||
--exclude 'output' \
|
||||
--exclude 'cache' \
|
||||
--exclude '.git' \
|
||||
--exclude 'deploy-temp' \
|
||||
--exclude '*.md' \
|
||||
--exclude 'bruno-tests' \
|
||||
--exclude 'test-*.js' \
|
||||
--exclude 'generate-*.js' \
|
||||
./ ./deploy-temp/
|
||||
|
||||
# Zum Server kopieren
|
||||
echo "Uploading to $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH..."
|
||||
ssh $DEPLOY_USER@$DEPLOY_HOST "mkdir -p $DEPLOY_PATH"
|
||||
rsync -avz ./deploy-temp/ $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/
|
||||
|
||||
# Fonts extra kopieren
|
||||
echo "Uploading fonts..."
|
||||
scp ./fonts/*.svg $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/fonts/
|
||||
|
||||
# Cleanup
|
||||
rm -rf ./deploy-temp
|
||||
|
||||
echo ""
|
||||
echo "Dateien erfolgreich hochgeladen!"
|
||||
echo ""
|
||||
echo "Nächste Schritte auf dem Server:"
|
||||
echo " ssh $DEPLOY_USER@$DEPLOY_HOST"
|
||||
echo " cd $DEPLOY_PATH"
|
||||
echo " nano .env # .env Datei erstellen!"
|
||||
echo " mkdir -p /var/skrift-output"
|
||||
echo " docker-compose up -d --build"
|
||||
}
|
||||
|
||||
# Funktion: Auf Server deployen (via Docker Hub)
|
||||
deploy_server() {
|
||||
check_config "push"
|
||||
|
||||
echo "Deploying to server via Docker Hub..."
|
||||
|
||||
# SSH Befehle auf Server ausführen
|
||||
ssh $DEPLOY_USER@$DEPLOY_HOST << ENDSSH
|
||||
set -e
|
||||
|
||||
echo "Creating directories..."
|
||||
mkdir -p $DEPLOY_PATH
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
cd $DEPLOY_PATH
|
||||
|
||||
echo "Checking if .env exists..."
|
||||
if [ ! -f .env ]; then
|
||||
echo "ERROR: .env Datei fehlt!"
|
||||
echo "Bitte zuerst .env erstellen mit:"
|
||||
echo " SCRIPTALIZER_LICENSE_KEY=..."
|
||||
echo " SCRIPTALIZER_ERR_FREQUENCY=0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking if docker-compose.yml exists..."
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "Creating docker-compose.yml..."
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
skrift-backend:
|
||||
image: $DOCKER_HUB_USER/$DOCKER_IMAGE_NAME:latest
|
||||
container_name: skrift-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=4000
|
||||
- SCRIPTALIZER_LICENSE_KEY=\${SCRIPTALIZER_LICENSE_KEY}
|
||||
- SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
- BATCH_SIZE=30
|
||||
- CACHE_LIFETIME_HOURS=2
|
||||
- RATE_LIMIT_PER_MINUTE=2
|
||||
volumes:
|
||||
- ./fonts:/app/fonts:ro
|
||||
- skrift-cache:/app/cache
|
||||
- /var/skrift-output:/app/output
|
||||
networks:
|
||||
- skrift-network
|
||||
|
||||
volumes:
|
||||
skrift-cache:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
skrift-network:
|
||||
driver: bridge
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Pulling latest image..."
|
||||
docker-compose pull
|
||||
|
||||
echo "Starting containers..."
|
||||
docker-compose up -d
|
||||
|
||||
echo "Waiting for container to be healthy..."
|
||||
sleep 5
|
||||
|
||||
echo "Checking health..."
|
||||
docker-compose ps
|
||||
|
||||
echo ""
|
||||
echo "Deployment complete!"
|
||||
echo "Check logs with: docker-compose logs -f"
|
||||
ENDSSH
|
||||
|
||||
echo ""
|
||||
echo "Deployment auf Server abgeschlossen!"
|
||||
echo ""
|
||||
echo "Health-Check:"
|
||||
echo " curl http://$DEPLOY_HOST:4000/health"
|
||||
}
|
||||
|
||||
# Funktion: SSH zum Server
|
||||
ssh_server() {
|
||||
check_config
|
||||
echo "Connecting to $DEPLOY_USER@$DEPLOY_HOST..."
|
||||
ssh $DEPLOY_USER@$DEPLOY_HOST
|
||||
}
|
||||
|
||||
# Main Script
|
||||
case "$1" in
|
||||
local)
|
||||
build_local
|
||||
;;
|
||||
push)
|
||||
push_dockerhub
|
||||
;;
|
||||
server-scp)
|
||||
deploy_scp
|
||||
;;
|
||||
server-deploy)
|
||||
deploy_server
|
||||
;;
|
||||
server-ssh)
|
||||
ssh_server
|
||||
;;
|
||||
help|--help|-h|"")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "Unbekannte Option: $1"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
37
Docker Backend/docker-compose.yml
Normal file
37
Docker Backend/docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
skrift-backend:
|
||||
build: .
|
||||
container_name: skrift-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- PORT=4000
|
||||
- SCRIPTALIZER_LICENSE_KEY=${SCRIPTALIZER_LICENSE_KEY}
|
||||
- SCRIPTALIZER_ERR_FREQUENCY=${SCRIPTALIZER_ERR_FREQUENCY:-10}
|
||||
- BATCH_SIZE=${BATCH_SIZE:-30}
|
||||
- CACHE_LIFETIME_HOURS=${CACHE_LIFETIME_HOURS:-2}
|
||||
- RATE_LIMIT_PER_MINUTE=${RATE_LIMIT_PER_MINUTE:-2}
|
||||
volumes:
|
||||
- ./fonts:/app/fonts:ro # SVG Fonts (read-only)
|
||||
- skrift-cache:/app/cache # Preview cache (temporary)
|
||||
- /var/skrift-output:/app/output # Output files for N8N
|
||||
networks:
|
||||
- skrift-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
|
||||
volumes:
|
||||
skrift-cache:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
skrift-network:
|
||||
driver: bridge
|
||||
2224
Docker Backend/fonts/alva.svg
Normal file
2224
Docker Backend/fonts/alva.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 152 KiB |
2033
Docker Backend/fonts/ellie.svg
Normal file
2033
Docker Backend/fonts/ellie.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 149 KiB |
2319
Docker Backend/fonts/tilda.svg
Normal file
2319
Docker Backend/fonts/tilda.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 152 KiB |
258
Docker Backend/generate-9-orders.js
Normal file
258
Docker Backend/generate-9-orders.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Generiert 9 Bestellungen mit allen Font/Format/Umschlag Kombinationen
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
|
||||
const API_URL = 'localhost';
|
||||
const API_PORT = 4000;
|
||||
|
||||
// Texte
|
||||
const TEXT_A4 = `Sehr geehrte Damen und Herren,
|
||||
|
||||
hiermit bestätigen wir den Eingang Ihrer Bestellung und bedanken uns herzlich für Ihr Vertrauen.
|
||||
|
||||
Ihre Bestellung wird schnellstmöglich bearbeitet und versendet. Sie erhalten in Kürze eine separate Versandbestätigung mit der Sendungsverfolgungsnummer.
|
||||
|
||||
Sollten Sie Fragen zu Ihrer Bestellung haben, stehen wir Ihnen jederzeit gerne zur Verfügung.
|
||||
|
||||
Wir wünschen Ihnen viel Freude mit Ihren bestellten Produkten und freuen uns auf eine weitere Zusammenarbeit.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
Ihr Skrift-Team`;
|
||||
|
||||
const TEXT_A6 = `Liebe Grüße!
|
||||
|
||||
Vielen Dank für Ihre Bestellung.
|
||||
|
||||
Wir wünschen Ihnen einen wundervollen Tag!
|
||||
|
||||
Herzlichst,
|
||||
Ihr Skrift-Team`;
|
||||
|
||||
const orders = [
|
||||
// TILDA
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-TILDA-A4',
|
||||
font: 'tilda',
|
||||
format: 'a4',
|
||||
text: TEXT_A4,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-TILDA-A6Q',
|
||||
font: 'tilda',
|
||||
format: 'a6l',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'din_lang',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-TILDA-A6H',
|
||||
font: 'tilda',
|
||||
format: 'a6p',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'custom'
|
||||
},
|
||||
// ALVA
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ALVA-A4',
|
||||
font: 'alva',
|
||||
format: 'a4',
|
||||
text: TEXT_A4,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ALVA-A6Q',
|
||||
font: 'alva',
|
||||
format: 'a6l',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'din_lang',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ALVA-A6H',
|
||||
font: 'alva',
|
||||
format: 'a6p',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'custom'
|
||||
},
|
||||
// ELLIE
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ELLIE-A4',
|
||||
font: 'ellie',
|
||||
format: 'a4',
|
||||
text: TEXT_A4,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ELLIE-A6Q',
|
||||
font: 'ellie',
|
||||
format: 'a6l',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'din_lang',
|
||||
envelopeType: 'recipient'
|
||||
},
|
||||
{
|
||||
orderNumber: 'SK-2026-01-02-ELLIE-A6H',
|
||||
font: 'ellie',
|
||||
format: 'a6p',
|
||||
text: TEXT_A6,
|
||||
envelopeFormat: 'c6',
|
||||
envelopeType: 'custom'
|
||||
}
|
||||
];
|
||||
|
||||
async function generateOrder(order) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const envelope = order.envelopeType === 'recipient'
|
||||
? {
|
||||
index: 0,
|
||||
format: order.envelopeFormat,
|
||||
font: order.font,
|
||||
type: 'recipient',
|
||||
data: {
|
||||
Vorname: 'Max',
|
||||
Nachname: 'Mustermann',
|
||||
Strasse: 'Hauptstraße 1',
|
||||
PLZ: '10115',
|
||||
Ort: 'Berlin'
|
||||
}
|
||||
}
|
||||
: {
|
||||
index: 0,
|
||||
format: order.envelopeFormat,
|
||||
font: order.font,
|
||||
type: 'custom',
|
||||
data: {
|
||||
customText: 'Für unsere geschätzten Kunden'
|
||||
}
|
||||
};
|
||||
|
||||
const requestBody = {
|
||||
orderNumber: order.orderNumber,
|
||||
letters: [
|
||||
{
|
||||
index: 0,
|
||||
format: order.format,
|
||||
font: order.font,
|
||||
text: order.text,
|
||||
placeholders: {}
|
||||
}
|
||||
],
|
||||
envelopes: [envelope]
|
||||
};
|
||||
|
||||
const body = JSON.stringify(requestBody);
|
||||
|
||||
const options = {
|
||||
hostname: API_URL,
|
||||
port: API_PORT,
|
||||
path: '/api/order/generate',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
},
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
resolve({ order: order.orderNumber, result });
|
||||
} catch (err) {
|
||||
reject({ order: order.orderNumber, error: err.message, data });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject({ order: order.orderNumber, error: err.message });
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject({ order: order.orderNumber, error: 'timeout' });
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function generateAllOrders() {
|
||||
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ GENERIERE 9 BESTELLUNGEN ║');
|
||||
console.log('║ 3 Fonts × 3 Formate = 9 Kombinationen ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
const order = orders[i];
|
||||
const num = i + 1;
|
||||
|
||||
console.log(`\n[${num}/9] Generiere: ${order.orderNumber}`);
|
||||
console.log(` Font: ${order.font.toUpperCase()}`);
|
||||
console.log(` Format: ${order.format.toUpperCase()}`);
|
||||
console.log(` Umschlag: ${order.envelopeFormat} (${order.envelopeType})`);
|
||||
|
||||
try {
|
||||
const result = await generateOrder(order);
|
||||
console.log(` ✅ Erfolgreich generiert`);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
console.log(` ❌ Fehler: ${err.error || err.message}`);
|
||||
results.push({ order: order.orderNumber, error: err });
|
||||
}
|
||||
|
||||
// Pause zwischen Requests (Rate Limiting)
|
||||
if (i < orders.length - 1) {
|
||||
console.log(` ⏳ Warte 3 Sekunden...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n\n' + '═'.repeat(60));
|
||||
console.log('ZUSAMMENFASSUNG');
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
const successful = results.filter(r => !r.error);
|
||||
const failed = results.filter(r => r.error);
|
||||
|
||||
console.log(`\n✅ Erfolgreich: ${successful.length}/9`);
|
||||
console.log(`❌ Fehlgeschlagen: ${failed.length}/9`);
|
||||
|
||||
if (successful.length > 0) {
|
||||
console.log('\n📦 Generierte Bestellungen:');
|
||||
successful.forEach(r => {
|
||||
console.log(` - ${r.order}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.log('\n❌ Fehlgeschlagene Bestellungen:');
|
||||
failed.forEach(r => {
|
||||
console.log(` - ${r.order}: ${r.error.error || r.error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log('\n💾 Alle Dateien in: E:\\Dokumente\\05_Skrift\\Frontend_Backend_Konfigurator\\Docker Backend\\output\\');
|
||||
}
|
||||
|
||||
// Start
|
||||
generateAllOrders().catch(console.error);
|
||||
1624
Docker Backend/package-lock.json
generated
Normal file
1624
Docker Backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Docker Backend/package.json
Normal file
31
Docker Backend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "skrift-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for Skrift handwritten document configurator",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test:separator": "node test-scriptalizer.js"
|
||||
},
|
||||
"keywords": [
|
||||
"skrift",
|
||||
"handwriting",
|
||||
"svg",
|
||||
"scriptalizer"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@paypal/paypal-server-sdk": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.0",
|
||||
"multer": "^2.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
406
Docker Backend/prepare-deployment.js
Normal file
406
Docker Backend/prepare-deployment.js
Normal file
@@ -0,0 +1,406 @@
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SOURCE_DIR = __dirname;
|
||||
const DEPLOY_DIR = path.join(__dirname, '..', 'DEPLOYMENT_READY');
|
||||
|
||||
// Dateien und Ordner die KOPIERT werden sollen
|
||||
const INCLUDE_FILES = [
|
||||
// Docker
|
||||
'Dockerfile',
|
||||
'docker-compose.yml',
|
||||
'.dockerignore',
|
||||
|
||||
// Package
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
|
||||
// Source Code
|
||||
'src/**/*',
|
||||
|
||||
// Fonts
|
||||
'fonts/**/*.svg',
|
||||
|
||||
// Config Example
|
||||
'.env.example'
|
||||
];
|
||||
|
||||
// Dateien die NICHT kopiert werden sollen
|
||||
const EXCLUDE_PATTERNS = [
|
||||
'node_modules',
|
||||
'output',
|
||||
'cache',
|
||||
'.git',
|
||||
'bruno-tests',
|
||||
'*.md',
|
||||
'test-*.js',
|
||||
'test-*.json',
|
||||
'test-*.sh',
|
||||
'generate-*.js',
|
||||
'server.log',
|
||||
'.env',
|
||||
'deploy.sh',
|
||||
'prepare-deployment.js',
|
||||
'DEPLOYMENT_READY'
|
||||
];
|
||||
|
||||
function shouldExclude(filePath) {
|
||||
const relativePath = path.relative(SOURCE_DIR, filePath);
|
||||
|
||||
return EXCLUDE_PATTERNS.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
||||
return regex.test(path.basename(filePath)) || regex.test(relativePath);
|
||||
}
|
||||
return relativePath.startsWith(pattern) || path.basename(filePath) === pattern;
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureDir(dir) {
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFile(src, dest) {
|
||||
await ensureDir(path.dirname(dest));
|
||||
await fs.copyFile(src, dest);
|
||||
}
|
||||
|
||||
async function removeDir(dir) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await removeDir(fullPath);
|
||||
} else {
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
}
|
||||
await fs.rmdir(dir);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(p) {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDirectory(src, dest) {
|
||||
const entries = await fs.readdir(src, { withFileTypes: true });
|
||||
|
||||
await ensureDir(dest);
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (shouldExclude(srcPath)) {
|
||||
console.log(`⏭️ Skipping: ${path.relative(SOURCE_DIR, srcPath)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await copyFile(srcPath, destPath);
|
||||
console.log(`✅ Copied: ${path.relative(SOURCE_DIR, srcPath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Preparing Deployment Package...\n');
|
||||
|
||||
// Deployment-Verzeichnis erstellen/leeren
|
||||
if (await pathExists(DEPLOY_DIR)) {
|
||||
console.log('🗑️ Cleaning existing deployment directory...');
|
||||
await removeDir(DEPLOY_DIR);
|
||||
}
|
||||
|
||||
await ensureDir(DEPLOY_DIR);
|
||||
console.log(`📁 Created: ${DEPLOY_DIR}\n`);
|
||||
|
||||
// Dateien kopieren
|
||||
console.log('📋 Copying production files...\n');
|
||||
await copyDirectory(SOURCE_DIR, DEPLOY_DIR);
|
||||
|
||||
// Produktions-.env.example erstellen
|
||||
const envExample = `# Skrift Backend - Production Environment Variables
|
||||
|
||||
# Scriptalizer API Configuration
|
||||
SCRIPTALIZER_LICENSE_KEY=f9918b40-d11c-11f0-b558-0800200c9a66
|
||||
SCRIPTALIZER_ERR_FREQUENCY=0
|
||||
|
||||
# Preview Settings
|
||||
BATCH_SIZE=30
|
||||
CACHE_LIFETIME_HOURS=2
|
||||
RATE_LIMIT_PER_MINUTE=2
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
PORT=4000
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(DEPLOY_DIR, '.env.example'), envExample);
|
||||
console.log('✅ Created: .env.example\n');
|
||||
|
||||
// README für Deployment erstellen
|
||||
const deployReadme = `# Skrift Backend - Deployment Package
|
||||
|
||||
Dieses Verzeichnis enthält alle notwendigen Dateien für das Deployment auf den Server.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### 1. Zum Server kopieren
|
||||
|
||||
\`\`\`bash
|
||||
# Mit SCP
|
||||
scp -r * root@DEIN-SERVER:/opt/skrift-backend/
|
||||
|
||||
# Oder mit rsync (falls verfügbar)
|
||||
rsync -avz ./ root@DEIN-SERVER:/opt/skrift-backend/
|
||||
\`\`\`
|
||||
|
||||
### 2. Auf dem Server einrichten
|
||||
|
||||
\`\`\`bash
|
||||
ssh root@DEIN-SERVER
|
||||
|
||||
cd /opt/skrift-backend
|
||||
|
||||
# .env erstellen (aus .env.example)
|
||||
cp .env.example .env
|
||||
nano .env # Prüfen und anpassen falls nötig
|
||||
|
||||
# Output-Verzeichnis erstellen
|
||||
mkdir -p /var/skrift-output
|
||||
chmod 755 /var/skrift-output
|
||||
|
||||
# Container starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Logs prüfen
|
||||
docker-compose logs -f
|
||||
\`\`\`
|
||||
|
||||
### 3. Testen
|
||||
|
||||
\`\`\`bash
|
||||
curl http://localhost:4000/health
|
||||
\`\`\`
|
||||
|
||||
## Enthaltene Dateien
|
||||
|
||||
- \`src/\` - Backend Source Code
|
||||
- \`fonts/\` - SVG Fonts (Tilda, Alva, Ellie)
|
||||
- \`Dockerfile\` - Docker Image Konfiguration
|
||||
- \`docker-compose.yml\` - Docker Compose Konfiguration
|
||||
- \`package.json\` - Node.js Dependencies
|
||||
- \`.env.example\` - Environment Variables Template
|
||||
|
||||
## Wichtig
|
||||
|
||||
⚠️ **SCRIPTALIZER_ERR_FREQUENCY=0** ist bereits gesetzt - keine durchgestrichenen Wörter!
|
||||
|
||||
## Nginx Proxy Manager
|
||||
|
||||
Nach dem Start in Nginx Proxy Manager konfigurieren:
|
||||
- Domain: backend.deine-domain.de
|
||||
- Forward to: skrift-backend:4000
|
||||
- SSL: Let's Encrypt aktivieren
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen: \`docker-compose logs -f\`
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(DEPLOY_DIR, 'README.txt'), deployReadme);
|
||||
console.log('✅ Created: README.txt\n');
|
||||
|
||||
// Upload-Script erstellen
|
||||
const uploadScript = `#!/bin/bash
|
||||
|
||||
# Skrift Backend - Upload Script
|
||||
# Dieses Script lädt das Backend auf den Server hoch
|
||||
|
||||
# KONFIGURATION - BITTE ANPASSEN!
|
||||
SERVER_USER="root"
|
||||
SERVER_HOST="" # z.B. "123.456.789.0" oder "dein-server.de"
|
||||
SERVER_PATH="/opt/skrift-backend"
|
||||
|
||||
# Farben für Output
|
||||
RED='\\033[0;31m'
|
||||
GREEN='\\033[0;32m'
|
||||
YELLOW='\\033[1;33m'
|
||||
NC='\\033[0m' # No Color
|
||||
|
||||
# Funktion: Fehler anzeigen und beenden
|
||||
error_exit() {
|
||||
echo -e "\${RED}❌ ERROR: \$1\${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prüfen ob Server konfiguriert ist
|
||||
if [ -z "$SERVER_HOST" ]; then
|
||||
error_exit "SERVER_HOST ist nicht gesetzt! Bitte in upload.sh die Variable SERVER_HOST setzen."
|
||||
fi
|
||||
|
||||
echo -e "\${GREEN}🚀 Skrift Backend Upload\${NC}"
|
||||
echo "======================================"
|
||||
echo "Server: \$SERVER_USER@\$SERVER_HOST"
|
||||
echo "Path: \$SERVER_PATH"
|
||||
echo ""
|
||||
|
||||
# Prüfen ob SSH-Verbindung funktioniert
|
||||
echo -e "\${YELLOW}🔍 Testing SSH connection...\${NC}"
|
||||
ssh -o ConnectTimeout=5 -o BatchMode=yes \$SERVER_USER@\$SERVER_HOST "echo '✅ SSH connection successful'" || error_exit "SSH connection failed"
|
||||
echo ""
|
||||
|
||||
# Verzeichnis auf Server erstellen
|
||||
echo -e "\${YELLOW}📁 Creating directory on server...\${NC}"
|
||||
ssh \$SERVER_USER@\$SERVER_HOST "mkdir -p \$SERVER_PATH" || error_exit "Failed to create directory"
|
||||
echo ""
|
||||
|
||||
# Dateien hochladen
|
||||
echo -e "\${YELLOW}📤 Uploading files...\${NC}"
|
||||
scp -r * \$SERVER_USER@\$SERVER_HOST:\$SERVER_PATH/ || error_exit "Upload failed"
|
||||
echo ""
|
||||
|
||||
echo -e "\${GREEN}✅ Upload successful!\${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. SSH to server: ssh \$SERVER_USER@\$SERVER_HOST"
|
||||
echo "2. Go to directory: cd \$SERVER_PATH"
|
||||
echo "3. Create .env: cp .env.example .env"
|
||||
echo "4. Create output dir: mkdir -p /var/skrift-output"
|
||||
echo "5. Start container: docker-compose up -d --build"
|
||||
echo "6. Check logs: docker-compose logs -f"
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(DEPLOY_DIR, 'upload.sh'), uploadScript);
|
||||
await fs.chmod(path.join(DEPLOY_DIR, 'upload.sh'), 0o755);
|
||||
console.log('✅ Created: upload.sh\n');
|
||||
|
||||
// Windows Batch Upload-Script
|
||||
const uploadBat = `@echo off
|
||||
REM Skrift Backend - Windows Upload Script
|
||||
|
||||
REM KONFIGURATION - BITTE ANPASSEN!
|
||||
set SERVER_USER=root
|
||||
set SERVER_HOST=
|
||||
set SERVER_PATH=/opt/skrift-backend
|
||||
|
||||
if "%SERVER_HOST%"=="" (
|
||||
echo ERROR: SERVER_HOST ist nicht gesetzt!
|
||||
echo Bitte in upload.bat die Variable SERVER_HOST setzen.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ========================================
|
||||
echo Skrift Backend Upload
|
||||
echo ========================================
|
||||
echo Server: %SERVER_USER%@%SERVER_HOST%
|
||||
echo Path: %SERVER_PATH%
|
||||
echo.
|
||||
|
||||
echo Uploading files...
|
||||
echo.
|
||||
|
||||
scp -r * %SERVER_USER%@%SERVER_HOST%:%SERVER_PATH%/
|
||||
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo.
|
||||
echo ERROR: Upload fehlgeschlagen!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Upload erfolgreich!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Naechste Schritte:
|
||||
echo 1. SSH to server: ssh %SERVER_USER%@%SERVER_HOST%
|
||||
echo 2. Go to directory: cd %SERVER_PATH%
|
||||
echo 3. Create .env: cp .env.example .env
|
||||
echo 4. Create output dir: mkdir -p /var/skrift-output
|
||||
echo 5. Start container: docker-compose up -d --build
|
||||
echo 6. Check logs: docker-compose logs -f
|
||||
echo.
|
||||
pause
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(DEPLOY_DIR, 'upload.bat'), uploadBat);
|
||||
console.log('✅ Created: upload.bat\n');
|
||||
|
||||
// Statistik
|
||||
const stats = await getDirectoryStats(DEPLOY_DIR);
|
||||
|
||||
console.log('');
|
||||
console.log('📊 Deployment Package Stats:');
|
||||
console.log('=====================================');
|
||||
console.log(`📁 Total Files: ${stats.files}`);
|
||||
console.log(`📂 Total Directories: ${stats.dirs}`);
|
||||
console.log(`💾 Total Size: ${formatBytes(stats.size)}`);
|
||||
console.log('');
|
||||
console.log('✅ Deployment Package Ready!');
|
||||
console.log('');
|
||||
console.log(`📦 Location: ${DEPLOY_DIR}`);
|
||||
console.log('');
|
||||
console.log('Next steps:');
|
||||
console.log('1. Gehe ins Verzeichnis: cd DEPLOYMENT_READY');
|
||||
console.log('2. Passe upload.sh oder upload.bat an (SERVER_HOST setzen)');
|
||||
console.log('3. Führe aus: ./upload.sh (Linux/Mac) oder upload.bat (Windows)');
|
||||
}
|
||||
|
||||
async function getDirectoryStats(dir) {
|
||||
let files = 0;
|
||||
let dirs = 0;
|
||||
let size = 0;
|
||||
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
dirs++;
|
||||
const subStats = await getDirectoryStats(fullPath);
|
||||
files += subStats.files;
|
||||
dirs += subStats.dirs;
|
||||
size += subStats.size;
|
||||
} else {
|
||||
files++;
|
||||
const stat = await fs.stat(fullPath);
|
||||
size += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
return { files, dirs, size };
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
414
Docker Backend/src/api/controllers/order-controller.js
Normal file
414
Docker Backend/src/api/controllers/order-controller.js
Normal file
@@ -0,0 +1,414 @@
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('../../config');
|
||||
const { scriptalizeBatch } = require('../../services/scriptalizer-service');
|
||||
const { generateLetterSVG, generateEnvelopeSVG } = require('../../lib/svg-generator');
|
||||
const { replacePlaceholders, generateAllCSVs } = require('../../services/placeholder-service');
|
||||
|
||||
/**
|
||||
* POST /api/order/finalize
|
||||
* Finalize order by copying cached previews to output directory
|
||||
*/
|
||||
async function finalizeOrder(req, res, next) {
|
||||
try {
|
||||
const { sessionId, orderNumber } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!sessionId || !orderNumber) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'sessionId and orderNumber are required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Order] Finalizing order: ${orderNumber} from session: ${sessionId}`);
|
||||
if (envelopes && envelopes.length > 0) {
|
||||
console.log(`[Order] Envelope data provided: ${envelopes.length} envelopes`);
|
||||
}
|
||||
|
||||
// Check if session cache exists
|
||||
const sessionDir = path.join(config.paths.previews, sessionId);
|
||||
|
||||
try {
|
||||
await fs.access(sessionDir);
|
||||
} catch {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
message: `Preview cache not found for session: ${sessionId}. Please generate previews first.`
|
||||
});
|
||||
}
|
||||
|
||||
// Check cache expiration
|
||||
const metadataPath = path.join(sessionDir, '.metadata.json');
|
||||
try {
|
||||
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
||||
const metadata = JSON.parse(metadataContent);
|
||||
|
||||
const expiresAt = new Date(metadata.expiresAt);
|
||||
if (new Date() > expiresAt) {
|
||||
return res.status(410).json({
|
||||
error: 'Cache expired',
|
||||
message: 'Preview cache has expired. Please regenerate previews.',
|
||||
expiredAt: metadata.expiresAt
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[Order] Metadata not found for session: ${sessionId}`);
|
||||
}
|
||||
|
||||
// Create output directory and subdirectories
|
||||
const outputDir = path.join(config.paths.output, orderNumber);
|
||||
const envelopesDir = path.join(outputDir, 'umschlaege');
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.mkdir(envelopesDir, { recursive: true });
|
||||
|
||||
// Copy files from cache to output, separating letters and envelopes
|
||||
const files = await fs.readdir(sessionDir);
|
||||
const copiedLetters = [];
|
||||
const copiedEnvelopes = [];
|
||||
const copiedOther = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Skip metadata file
|
||||
if (file === '.metadata.json') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourcePath = path.join(sessionDir, file);
|
||||
|
||||
// Determine destination based on file type
|
||||
if (file.startsWith('envelope_')) {
|
||||
// Umschläge in Unterordner 'umschlaege'
|
||||
const destPath = path.join(envelopesDir, file);
|
||||
await fs.copyFile(sourcePath, destPath);
|
||||
copiedEnvelopes.push(file);
|
||||
} else if (file.startsWith('letter_')) {
|
||||
// Briefe im Hauptordner
|
||||
const destPath = path.join(outputDir, file);
|
||||
await fs.copyFile(sourcePath, destPath);
|
||||
copiedLetters.push(file);
|
||||
} else {
|
||||
// Andere Dateien (CSV, etc.) im Hauptordner
|
||||
const destPath = path.join(outputDir, file);
|
||||
await fs.copyFile(sourcePath, destPath);
|
||||
copiedOther.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Create order metadata
|
||||
const orderMetadata = {
|
||||
orderNumber,
|
||||
sessionId,
|
||||
finalizedAt: new Date().toISOString(),
|
||||
letterCount: copiedLetters.length,
|
||||
envelopeCount: copiedEnvelopes.length,
|
||||
letters: copiedLetters,
|
||||
envelopes: copiedEnvelopes,
|
||||
other: copiedOther
|
||||
};
|
||||
|
||||
const orderMetadataPath = path.join(outputDir, 'order-metadata.json');
|
||||
await fs.writeFile(orderMetadataPath, JSON.stringify(orderMetadata, null, 2), 'utf8');
|
||||
|
||||
const totalFiles = copiedLetters.length + copiedEnvelopes.length + copiedOther.length;
|
||||
console.log(`[Order] Finalized order ${orderNumber}: ${copiedLetters.length} letters, ${copiedEnvelopes.length} envelopes, ${copiedOther.length} other files`);
|
||||
|
||||
// Response
|
||||
res.status(200).json({
|
||||
orderNumber,
|
||||
outputPath: outputDir,
|
||||
letters: copiedLetters,
|
||||
envelopes: copiedEnvelopes,
|
||||
other: copiedOther,
|
||||
totalFiles,
|
||||
envelopesGenerated: 0,
|
||||
timestamp: orderMetadata.finalizedAt
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Order] Error finalizing order:', err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/order/generate
|
||||
* Generate order from scratch without using cache
|
||||
*/
|
||||
async function generateOrder(req, res, next) {
|
||||
try {
|
||||
const { orderNumber, letters } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!orderNumber) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'orderNumber is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!letters || !Array.isArray(letters) || letters.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'Letters array is required and must not be empty'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Order] Generating order: ${orderNumber}`);
|
||||
console.log(`[Order] Total documents: ${letters.length}`);
|
||||
|
||||
// Debug: Zeige welche Dokument-Typen ankommen
|
||||
const letterCount = letters.filter(l => l.type !== 'envelope').length;
|
||||
const envelopeCount = letters.filter(l => l.type === 'envelope').length;
|
||||
console.log(`[Order] Letters: ${letterCount}, Envelopes: ${envelopeCount}`);
|
||||
console.log(`[Order] Document types:`, letters.map(l => ({ index: l.index, type: l.type, envelopeType: l.envelopeType })));
|
||||
|
||||
// Create output directory and subdirectories
|
||||
const outputDir = path.join(config.paths.output, orderNumber);
|
||||
const envelopesDir = path.join(outputDir, 'umschlaege');
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.mkdir(envelopesDir, { recursive: true });
|
||||
|
||||
// Step 1: Prepare texts and replace placeholders
|
||||
const processedLetters = letters.map((letter, loopIndex) => {
|
||||
let text = letter.text || '';
|
||||
|
||||
// Replace placeholders if present
|
||||
if (letter.placeholders && typeof letter.placeholders === 'object') {
|
||||
text = replacePlaceholders(text, letter.placeholders);
|
||||
}
|
||||
|
||||
return {
|
||||
...letter,
|
||||
// Behalte den ursprünglichen Index vom Frontend, oder verwende loopIndex als Fallback
|
||||
index: letter.index !== undefined ? letter.index : loopIndex,
|
||||
loopIndex, // Für scriptalizedMap Zuordnung
|
||||
processedText: text,
|
||||
format: letter.format || 'a4',
|
||||
font: letter.font || 'tilda',
|
||||
type: letter.type || 'letter',
|
||||
envelopeType: letter.envelopeType || 'recipient'
|
||||
};
|
||||
});
|
||||
|
||||
// Step 2: Group letters by font for batch scriptalization
|
||||
const lettersByFont = {};
|
||||
|
||||
processedLetters.forEach(letter => {
|
||||
const font = letter.font;
|
||||
if (!lettersByFont[font]) {
|
||||
lettersByFont[font] = [];
|
||||
}
|
||||
lettersByFont[font].push(letter);
|
||||
});
|
||||
|
||||
// Step 3: Scriptalize texts by font
|
||||
const scriptalizedMap = new Map();
|
||||
|
||||
for (const [font, fontLetters] of Object.entries(lettersByFont)) {
|
||||
console.log(`[Order] Scriptalizing ${fontLetters.length} texts with font: ${font}`);
|
||||
|
||||
const textsToScriptalize = fontLetters.map(l => l.processedText);
|
||||
|
||||
try {
|
||||
const scriptalizedTexts = await scriptalizeBatch(textsToScriptalize, font, config.scriptalizer.errFrequency);
|
||||
|
||||
fontLetters.forEach((letter, i) => {
|
||||
// Verwende loopIndex für die Map-Zuordnung, da das der Index im processedLetters Array ist
|
||||
scriptalizedMap.set(letter.loopIndex, scriptalizedTexts[i] || letter.processedText);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[Order] Scriptalization failed for font ${font}:`, err.message);
|
||||
return res.status(500).json({
|
||||
error: 'Scriptalization failed',
|
||||
message: `Failed to scriptalize texts with font ${font}: ${err.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Generate SVG files - separate letters and envelopes
|
||||
const generatedLetters = [];
|
||||
const generatedEnvelopes = [];
|
||||
|
||||
for (let i = 0; i < processedLetters.length; i++) {
|
||||
const letter = processedLetters[i];
|
||||
// Verwende loopIndex für scriptalizedMap Zuordnung
|
||||
const scriptalizedText = scriptalizedMap.get(letter.loopIndex) || letter.processedText;
|
||||
|
||||
let svgContent;
|
||||
|
||||
try {
|
||||
if (letter.type === 'envelope') {
|
||||
svgContent = generateEnvelopeSVG(
|
||||
scriptalizedText,
|
||||
letter.format,
|
||||
letter.envelopeType,
|
||||
{ font: letter.font }
|
||||
);
|
||||
} else {
|
||||
svgContent = generateLetterSVG(
|
||||
scriptalizedText,
|
||||
letter.format,
|
||||
{ font: letter.font }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Order] SVG generation failed for letter ${i}:`, err.message);
|
||||
return res.status(500).json({
|
||||
error: 'SVG generation failed',
|
||||
message: `Failed to generate SVG for letter ${i}: ${err.message}`
|
||||
});
|
||||
}
|
||||
|
||||
// Save SVG file - unterscheide zwischen Briefen und Umschlägen
|
||||
const absoluteIndex = letter.index !== undefined ? letter.index : i;
|
||||
|
||||
if (letter.type === 'envelope') {
|
||||
const filename = `envelope_${String(absoluteIndex).padStart(3, '0')}.svg`;
|
||||
const filepath = path.join(envelopesDir, filename);
|
||||
await fs.writeFile(filepath, svgContent, 'utf8');
|
||||
generatedEnvelopes.push(filename);
|
||||
} else {
|
||||
const filename = `letter_${String(absoluteIndex).padStart(3, '0')}.svg`;
|
||||
const filepath = path.join(outputDir, filename);
|
||||
await fs.writeFile(filepath, svgContent, 'utf8');
|
||||
generatedLetters.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Order] Generated ${generatedLetters.length} letters, ${generatedEnvelopes.length} envelopes`);
|
||||
|
||||
// Step 5: Generate all CSV files if placeholders are present
|
||||
const hasPlaceholders = processedLetters.some(l => l.placeholders && Object.keys(l.placeholders).length > 0);
|
||||
const csvFiles = [];
|
||||
|
||||
if (hasPlaceholders) {
|
||||
try {
|
||||
const csvs = generateAllCSVs(processedLetters);
|
||||
|
||||
// Brief-Platzhalter CSV
|
||||
if (csvs.placeholders) {
|
||||
const filename = 'brief_platzhalter.csv';
|
||||
await fs.writeFile(path.join(outputDir, filename), csvs.placeholders, 'utf8');
|
||||
csvFiles.push(filename);
|
||||
console.log(`[Order] Generated brief_platzhalter.csv`);
|
||||
}
|
||||
|
||||
// Empfänger CSV (für recipientData-Modus)
|
||||
if (csvs.recipients) {
|
||||
const filename = 'empfaenger.csv';
|
||||
await fs.writeFile(path.join(outputDir, filename), csvs.recipients, 'utf8');
|
||||
csvFiles.push(filename);
|
||||
console.log(`[Order] Generated empfaenger.csv`);
|
||||
}
|
||||
|
||||
// Umschlag-Platzhalter CSV (für customText-Modus)
|
||||
if (csvs.envelopePlaceholders) {
|
||||
const filename = 'umschlag_platzhalter.csv';
|
||||
await fs.writeFile(path.join(outputDir, filename), csvs.envelopePlaceholders, 'utf8');
|
||||
csvFiles.push(filename);
|
||||
console.log(`[Order] Generated umschlag_platzhalter.csv`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Order] CSV generation failed:', err.message);
|
||||
// Don't fail the request, CSV is optional
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Create order metadata
|
||||
const orderMetadata = {
|
||||
orderNumber,
|
||||
generatedAt: new Date().toISOString(),
|
||||
letterCount: generatedLetters.length,
|
||||
envelopeCount: generatedEnvelopes.length,
|
||||
letters: generatedLetters,
|
||||
envelopes: generatedEnvelopes,
|
||||
fonts: Object.keys(lettersByFont),
|
||||
hasPlaceholders,
|
||||
csvFiles
|
||||
};
|
||||
|
||||
const orderMetadataPath = path.join(outputDir, 'order-metadata.json');
|
||||
await fs.writeFile(orderMetadataPath, JSON.stringify(orderMetadata, null, 2), 'utf8');
|
||||
|
||||
console.log(`[Order] Order ${orderNumber} generated successfully`);
|
||||
|
||||
// Response
|
||||
res.status(200).json({
|
||||
orderNumber,
|
||||
outputPath: outputDir,
|
||||
letters: generatedLetters,
|
||||
envelopes: generatedEnvelopes,
|
||||
csvFiles,
|
||||
totalFiles: generatedLetters.length + generatedEnvelopes.length + csvFiles.length,
|
||||
timestamp: orderMetadata.generatedAt
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Order] Error generating order:', err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/order/motif
|
||||
* Upload a motif image for an order
|
||||
*/
|
||||
async function uploadMotif(req, res, next) {
|
||||
try {
|
||||
const { orderNumber } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!orderNumber) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'orderNumber is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid request',
|
||||
message: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Order] Uploading motif for order: ${orderNumber}`);
|
||||
console.log(`[Order] File: ${req.file.originalname}, Size: ${req.file.size} bytes, Type: ${req.file.mimetype}`);
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
const outputDir = path.join(config.paths.output, orderNumber);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Determine file extension
|
||||
const originalExt = path.extname(req.file.originalname).toLowerCase() || '.png';
|
||||
const filename = `motif${originalExt}`;
|
||||
const filepath = path.join(outputDir, filename);
|
||||
|
||||
// Write file to disk
|
||||
await fs.writeFile(filepath, req.file.buffer);
|
||||
|
||||
console.log(`[Order] Motif saved to: ${filepath}`);
|
||||
|
||||
// Response
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
orderNumber,
|
||||
filename,
|
||||
path: filepath,
|
||||
size: req.file.size,
|
||||
mimetype: req.file.mimetype
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Order] Error uploading motif:', err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
finalizeOrder,
|
||||
generateOrder,
|
||||
uploadMotif
|
||||
};
|
||||
200
Docker Backend/src/api/controllers/paypal-controller.js
Normal file
200
Docker Backend/src/api/controllers/paypal-controller.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const {
|
||||
Client,
|
||||
Environment,
|
||||
LogLevel,
|
||||
OrdersController
|
||||
} = require('@paypal/paypal-server-sdk');
|
||||
const config = require('../../config');
|
||||
|
||||
// PayPal Client initialisieren
|
||||
let client = null;
|
||||
let ordersController = null;
|
||||
|
||||
function initializePayPalClient() {
|
||||
if (client) return;
|
||||
|
||||
const { clientId, clientSecret, environment } = config.paypal;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
console.warn('[PayPal] Client ID oder Secret nicht konfiguriert');
|
||||
return;
|
||||
}
|
||||
|
||||
client = new Client({
|
||||
clientCredentialsAuthCredentials: {
|
||||
oAuthClientId: clientId,
|
||||
oAuthClientSecret: clientSecret,
|
||||
},
|
||||
timeout: 0,
|
||||
environment: environment === 'live' ? Environment.Production : Environment.Sandbox,
|
||||
logging: {
|
||||
logLevel: LogLevel.Info,
|
||||
logRequest: { logBody: true },
|
||||
logResponse: { logHeaders: true },
|
||||
},
|
||||
});
|
||||
|
||||
ordersController = new OrdersController(client);
|
||||
console.log(`[PayPal] Client initialisiert (${environment})`);
|
||||
}
|
||||
|
||||
// Initialisierung beim Laden des Moduls
|
||||
initializePayPalClient();
|
||||
|
||||
/**
|
||||
* Erstellt eine PayPal-Bestellung
|
||||
* POST /api/paypal/orders
|
||||
*/
|
||||
async function createOrder(req, res) {
|
||||
try {
|
||||
if (!ordersController) {
|
||||
return res.status(503).json({
|
||||
error: 'PayPal nicht konfiguriert',
|
||||
message: 'PayPal Client ID und Secret müssen in den Umgebungsvariablen gesetzt sein'
|
||||
});
|
||||
}
|
||||
|
||||
const { amount, currency = 'EUR', orderData } = req.body;
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Ungültiger Betrag',
|
||||
message: 'Der Bestellbetrag muss größer als 0 sein'
|
||||
});
|
||||
}
|
||||
|
||||
// Betrag auf 2 Dezimalstellen formatieren
|
||||
const formattedAmount = parseFloat(amount).toFixed(2);
|
||||
|
||||
// Produktbeschreibung erstellen
|
||||
const productName = orderData?.product?.label || 'Skrift Handschriftservice';
|
||||
const quantity = orderData?.quantity || 1;
|
||||
|
||||
const collect = {
|
||||
body: {
|
||||
intent: 'CAPTURE',
|
||||
purchaseUnits: [
|
||||
{
|
||||
amount: {
|
||||
currencyCode: currency,
|
||||
value: formattedAmount,
|
||||
breakdown: {
|
||||
itemTotal: {
|
||||
currencyCode: currency,
|
||||
value: formattedAmount,
|
||||
},
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
name: productName,
|
||||
unitAmount: {
|
||||
currencyCode: currency,
|
||||
value: formattedAmount,
|
||||
},
|
||||
quantity: '1',
|
||||
description: `${quantity}x ${productName}`,
|
||||
sku: orderData?.product?.key || 'skrift-order',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
paymentSource: {
|
||||
paypal: {
|
||||
experienceContext: {
|
||||
userAction: 'PAY_NOW',
|
||||
brandName: 'Skrift',
|
||||
locale: 'de-DE',
|
||||
landingPage: 'NO_PREFERENCE',
|
||||
shippingPreference: 'NO_SHIPPING',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prefer: 'return=minimal',
|
||||
};
|
||||
|
||||
console.log('[PayPal] Creating order:', { amount: formattedAmount, currency });
|
||||
|
||||
const { body, ...httpResponse } = await ordersController.createOrder(collect);
|
||||
const jsonResponse = JSON.parse(body);
|
||||
|
||||
console.log('[PayPal] Order created:', jsonResponse.id);
|
||||
|
||||
res.status(httpResponse.statusCode).json(jsonResponse);
|
||||
} catch (error) {
|
||||
console.error('[PayPal] Create order error:', error.message);
|
||||
res.status(500).json({
|
||||
error: 'Bestellung konnte nicht erstellt werden',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erfasst eine PayPal-Zahlung
|
||||
* POST /api/paypal/orders/:orderID/capture
|
||||
*/
|
||||
async function captureOrder(req, res) {
|
||||
try {
|
||||
if (!ordersController) {
|
||||
return res.status(503).json({
|
||||
error: 'PayPal nicht konfiguriert',
|
||||
message: 'PayPal Client ID und Secret müssen in den Umgebungsvariablen gesetzt sein'
|
||||
});
|
||||
}
|
||||
|
||||
const { orderID } = req.params;
|
||||
|
||||
if (!orderID) {
|
||||
return res.status(400).json({
|
||||
error: 'Order ID fehlt',
|
||||
message: 'Die PayPal Order ID muss angegeben werden'
|
||||
});
|
||||
}
|
||||
|
||||
const collect = {
|
||||
id: orderID,
|
||||
prefer: 'return=minimal',
|
||||
};
|
||||
|
||||
console.log('[PayPal] Capturing order:', orderID);
|
||||
|
||||
const { body, ...httpResponse } = await ordersController.captureOrder(collect);
|
||||
const jsonResponse = JSON.parse(body);
|
||||
|
||||
console.log('[PayPal] Order captured:', {
|
||||
id: jsonResponse.id,
|
||||
status: jsonResponse.status
|
||||
});
|
||||
|
||||
res.status(httpResponse.statusCode).json(jsonResponse);
|
||||
} catch (error) {
|
||||
console.error('[PayPal] Capture order error:', error.message);
|
||||
res.status(500).json({
|
||||
error: 'Zahlung konnte nicht erfasst werden',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den PayPal-Konfigurationsstatus
|
||||
* GET /api/paypal/status
|
||||
*/
|
||||
function getStatus(req, res) {
|
||||
const { clientId, environment } = config.paypal;
|
||||
|
||||
res.json({
|
||||
configured: !!(clientId && config.paypal.clientSecret),
|
||||
environment,
|
||||
// Nur die ersten/letzten Zeichen der Client ID anzeigen
|
||||
clientIdPreview: clientId ? `${clientId.substring(0, 8)}...${clientId.substring(clientId.length - 4)}` : null
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createOrder,
|
||||
captureOrder,
|
||||
getStatus
|
||||
};
|
||||
322
Docker Backend/src/api/controllers/preview-controller.js
Normal file
322
Docker Backend/src/api/controllers/preview-controller.js
Normal file
@@ -0,0 +1,322 @@
|
||||
const fs = require("fs").promises;
|
||||
const fsSync = require("fs");
|
||||
const path = require("path");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const config = require("../../config");
|
||||
const { scriptalizeBatch } = require("../../services/scriptalizer-service");
|
||||
const {
|
||||
generateLetterSVG,
|
||||
generateEnvelopeSVG,
|
||||
} = require("../../lib/svg-generator");
|
||||
const {
|
||||
replacePlaceholders,
|
||||
generateAllCSVs,
|
||||
} = require("../../services/placeholder-service");
|
||||
|
||||
/**
|
||||
* POST /api/preview/batch
|
||||
* Generate preview batch with rate limiting
|
||||
*/
|
||||
async function generateBatch(req, res, next) {
|
||||
try {
|
||||
const { sessionId, letters } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!letters || !Array.isArray(letters) || letters.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request",
|
||||
message: "Letters array is required and must not be empty",
|
||||
});
|
||||
}
|
||||
|
||||
// KEIN Limit mehr - Frontend sendet ALLE Dokumente auf einmal
|
||||
// Backend teilt sie intern in 25er Batches für Scriptalizer auf
|
||||
console.log(`[Preview] Received ${letters.length} letters (no limit)`);
|
||||
|
||||
// Optional: Warn if extremely large
|
||||
if (letters.length > 1000) {
|
||||
console.warn(`[Preview] Large batch detected: ${letters.length} letters`);
|
||||
}
|
||||
|
||||
// Generate or validate sessionId
|
||||
const finalSessionId = sessionId || uuidv4();
|
||||
|
||||
console.log(
|
||||
`[Preview] Starting batch generation for session: ${finalSessionId}`
|
||||
);
|
||||
console.log(`[Preview] Batch size: ${letters.length} letters`);
|
||||
|
||||
// Create session cache directory
|
||||
const sessionDir = path.join(config.paths.previews, finalSessionId);
|
||||
await fs.mkdir(sessionDir, { recursive: true });
|
||||
|
||||
// Step 1: Prepare texts and replace placeholders
|
||||
console.log(
|
||||
"[Preview] Received indices from frontend:",
|
||||
letters.map((l) => l.index)
|
||||
);
|
||||
|
||||
const processedLetters = letters.map((letter, loopIndex) => {
|
||||
let text = letter.text || "";
|
||||
|
||||
// Replace placeholders if present
|
||||
if (letter.placeholders && typeof letter.placeholders === "object") {
|
||||
text = replacePlaceholders(text, letter.placeholders);
|
||||
}
|
||||
|
||||
return {
|
||||
...letter,
|
||||
// Verwende den vom Frontend gesendeten Index, oder fallback auf loopIndex
|
||||
index: letter.index !== undefined ? letter.index : loopIndex,
|
||||
processedText: text,
|
||||
format: letter.format || "a4",
|
||||
font: letter.font || "tilda",
|
||||
type: letter.type || "letter",
|
||||
envelopeType: letter.envelopeType || "recipient",
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[Preview] Final indices being used for files:",
|
||||
processedLetters.map((l) => l.index)
|
||||
);
|
||||
|
||||
// Step 2: Batch scriptalize texts
|
||||
const textsToScriptalize = processedLetters.map((l) => l.processedText);
|
||||
const font = processedLetters[0]?.font || "tilda";
|
||||
|
||||
console.log(
|
||||
`[Preview] Scriptalizing ${textsToScriptalize.length} texts with font: ${font}`
|
||||
);
|
||||
|
||||
let scriptalizedTexts;
|
||||
try {
|
||||
scriptalizedTexts = await scriptalizeBatch(
|
||||
textsToScriptalize,
|
||||
font,
|
||||
config.scriptalizer.errFrequency
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[Preview] Scriptalization failed:", err.message);
|
||||
return res.status(500).json({
|
||||
error: "Scriptalization failed",
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (scriptalizedTexts.length !== processedLetters.length) {
|
||||
console.warn(
|
||||
`[Preview] Scriptalization mismatch: expected ${processedLetters.length}, got ${scriptalizedTexts.length}`
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Generate SVG files
|
||||
const files = [];
|
||||
let hasOverflow = false;
|
||||
|
||||
for (let i = 0; i < processedLetters.length; i++) {
|
||||
const letter = processedLetters[i];
|
||||
const scriptalizedText = scriptalizedTexts[i] || letter.processedText;
|
||||
|
||||
let svgResult;
|
||||
|
||||
try {
|
||||
if (letter.type === "envelope") {
|
||||
// Umschläge haben keine Zeilenbegrenzung - nur SVG zurückgeben
|
||||
const svgContent = generateEnvelopeSVG(
|
||||
scriptalizedText,
|
||||
letter.format,
|
||||
letter.envelopeType,
|
||||
{ font: letter.font }
|
||||
);
|
||||
svgResult = { svg: svgContent, lineCount: 0, lineLimit: 0, overflow: false };
|
||||
} else {
|
||||
// Briefe: Zeileninfo mit zurückgeben
|
||||
svgResult = generateLetterSVG(scriptalizedText, letter.format, {
|
||||
font: letter.font,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[Preview] SVG generation failed for letter ${i}:`,
|
||||
err.message
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "SVG generation failed",
|
||||
message: `Failed to generate SVG for letter ${i}: ${err.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Track overflow
|
||||
if (svgResult.overflow) {
|
||||
hasOverflow = true;
|
||||
console.log(`[Preview] Letter ${i} has overflow: ${svgResult.lineCount}/${svgResult.lineLimit} lines`);
|
||||
}
|
||||
|
||||
// Save SVG file - verwende den absoluten Index aus letter
|
||||
// Unterscheide zwischen Briefen und Umschlägen im Dateinamen
|
||||
const absoluteIndex = letter.index !== undefined ? letter.index : i;
|
||||
const prefix = letter.type === 'envelope' ? 'envelope' : 'letter';
|
||||
const filename = `${prefix}_${String(absoluteIndex).padStart(3, "0")}.svg`;
|
||||
const filepath = path.join(sessionDir, filename);
|
||||
|
||||
await fs.writeFile(filepath, svgResult.svg, "utf8");
|
||||
|
||||
// Empfänger-Info für bessere Fehlermeldung
|
||||
const recipientName = letter.placeholders?.name || letter.placeholders?.vorname || `Brief ${absoluteIndex + 1}`;
|
||||
|
||||
files.push({
|
||||
index: absoluteIndex,
|
||||
filename,
|
||||
url: `/api/preview/${finalSessionId}/${filename}`,
|
||||
lineCount: svgResult.lineCount,
|
||||
lineLimit: svgResult.lineLimit,
|
||||
overflow: svgResult.overflow,
|
||||
recipientName: letter.type !== 'envelope' ? recipientName : null,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Preview] Generated ${files.length} SVG files`);
|
||||
|
||||
// Step 4: Generate all CSV files (placeholders, recipients, envelope placeholders, free addresses)
|
||||
const csvUrls = {};
|
||||
const hasPlaceholders = processedLetters.some((l) => l.placeholders && Object.keys(l.placeholders).length > 0);
|
||||
const hasFreeAddresses = processedLetters.some((l) => l.type === 'envelope' && l.envelopeType === 'free');
|
||||
|
||||
if (hasPlaceholders || hasFreeAddresses) {
|
||||
try {
|
||||
const csvs = generateAllCSVs(processedLetters);
|
||||
|
||||
// Brief-Platzhalter CSV
|
||||
if (csvs.placeholders) {
|
||||
const filename = "brief_platzhalter.csv";
|
||||
await fs.writeFile(path.join(sessionDir, filename), csvs.placeholders, "utf8");
|
||||
csvUrls.placeholders = `/api/preview/${finalSessionId}/${filename}`;
|
||||
console.log(`[Preview] Generated brief_platzhalter.csv`);
|
||||
}
|
||||
|
||||
// Empfänger CSV (für recipientData-Modus)
|
||||
if (csvs.recipients) {
|
||||
const filename = "empfaenger.csv";
|
||||
await fs.writeFile(path.join(sessionDir, filename), csvs.recipients, "utf8");
|
||||
csvUrls.recipients = `/api/preview/${finalSessionId}/${filename}`;
|
||||
console.log(`[Preview] Generated empfaenger.csv`);
|
||||
}
|
||||
|
||||
// Umschlag-Platzhalter CSV (für customText-Modus)
|
||||
if (csvs.envelopePlaceholders) {
|
||||
const filename = "umschlag_platzhalter.csv";
|
||||
await fs.writeFile(path.join(sessionDir, filename), csvs.envelopePlaceholders, "utf8");
|
||||
csvUrls.envelopePlaceholders = `/api/preview/${finalSessionId}/${filename}`;
|
||||
console.log(`[Preview] Generated umschlag_platzhalter.csv`);
|
||||
}
|
||||
|
||||
// Freie Adressen CSV (für free-Modus)
|
||||
if (csvs.freeAddresses) {
|
||||
const filename = "freie_adressen.csv";
|
||||
await fs.writeFile(path.join(sessionDir, filename), csvs.freeAddresses, "utf8");
|
||||
csvUrls.freeAddresses = `/api/preview/${finalSessionId}/${filename}`;
|
||||
console.log(`[Preview] Generated freie_adressen.csv`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Preview] CSV generation failed:", err.message);
|
||||
// Don't fail the request, CSV is optional
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Save session metadata (no expiration)
|
||||
const metadataPath = path.join(sessionDir, ".metadata.json");
|
||||
const metadata = {
|
||||
sessionId: finalSessionId,
|
||||
createdAt: new Date().toISOString(),
|
||||
letterCount: files.length,
|
||||
font,
|
||||
hasPlaceholders,
|
||||
};
|
||||
|
||||
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
|
||||
|
||||
console.log(
|
||||
`[Preview] Batch generation complete for session: ${finalSessionId}`
|
||||
);
|
||||
|
||||
// Response
|
||||
res.status(200).json({
|
||||
sessionId: finalSessionId,
|
||||
files,
|
||||
csvUrls,
|
||||
hasOverflow,
|
||||
overflowFiles: files.filter(f => f.overflow),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[Preview] Unexpected error:", err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/preview/:sessionId/:filename
|
||||
* Serve cached preview files
|
||||
*/
|
||||
async function servePreview(req, res, next) {
|
||||
try {
|
||||
const { sessionId, filename } = req.params;
|
||||
|
||||
// Validate inputs
|
||||
if (!sessionId || !filename) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request",
|
||||
message: "sessionId and filename are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent directory traversal
|
||||
const sanitizedFilename = path.basename(filename);
|
||||
const filePath = path.join(
|
||||
config.paths.previews,
|
||||
sessionId,
|
||||
sanitizedFilename
|
||||
);
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
return res.status(404).json({
|
||||
error: "File not found",
|
||||
message: `Preview file not found: ${sanitizedFilename}`,
|
||||
});
|
||||
}
|
||||
|
||||
// No cache expiration check - files are served until manually deleted
|
||||
|
||||
// Determine content type
|
||||
const ext = path.extname(sanitizedFilename).toLowerCase();
|
||||
let contentType = "application/octet-stream";
|
||||
|
||||
if (ext === ".svg") {
|
||||
contentType = "image/svg+xml";
|
||||
} else if (ext === ".csv") {
|
||||
contentType = "text/csv";
|
||||
}
|
||||
|
||||
// Serve file - NO CACHING
|
||||
res.setHeader("Content-Type", contentType);
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
|
||||
const fileContent = await fs.readFile(filePath);
|
||||
res.send(fileContent);
|
||||
} catch (err) {
|
||||
console.error("[Preview] Error serving file:", err);
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
// No automatic cache cleanup - files are kept until manually deleted
|
||||
|
||||
module.exports = {
|
||||
generateBatch,
|
||||
servePreview,
|
||||
};
|
||||
72
Docker Backend/src/api/middleware/auth.js
Normal file
72
Docker Backend/src/api/middleware/auth.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Authentication Middleware
|
||||
* Validiert API-Token in Request-Headers
|
||||
*/
|
||||
|
||||
const config = require('../../config');
|
||||
|
||||
/**
|
||||
* Middleware: API-Token validieren
|
||||
*/
|
||||
function authenticateApiToken(req, res, next) {
|
||||
// API-Token aus Umgebungsvariablen
|
||||
const validToken = config.auth.apiToken;
|
||||
|
||||
// Wenn kein Token konfiguriert ist, alle Requests erlauben (Development)
|
||||
if (!validToken) {
|
||||
console.warn('[Auth] WARNING: No API token configured - authentication disabled!');
|
||||
return next();
|
||||
}
|
||||
|
||||
// Token aus verschiedenen Quellen prüfen
|
||||
const token =
|
||||
req.headers['x-api-token'] || // Custom Header
|
||||
req.headers['authorization']?.replace('Bearer ', '') || // Bearer Token
|
||||
req.query.api_token; // Query Parameter (nur für Tests)
|
||||
|
||||
// Kein Token vorhanden
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
error: 'Unauthorized',
|
||||
message: 'API token required. Please provide X-API-Token header or Authorization: Bearer <token>',
|
||||
});
|
||||
}
|
||||
|
||||
// Token validieren
|
||||
if (token !== validToken) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Invalid API token',
|
||||
});
|
||||
}
|
||||
|
||||
// Token ist gültig
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionale Authentifizierung - Warnung aber kein Fehler
|
||||
*/
|
||||
function optionalAuth(req, res, next) {
|
||||
const validToken = config.auth.apiToken;
|
||||
|
||||
if (!validToken) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const token =
|
||||
req.headers['x-api-token'] ||
|
||||
req.headers['authorization']?.replace('Bearer ', '') ||
|
||||
req.query.api_token;
|
||||
|
||||
if (!token || token !== validToken) {
|
||||
console.warn('[Auth] Unauthenticated request to', req.path);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticateApiToken,
|
||||
optionalAuth,
|
||||
};
|
||||
13
Docker Backend/src/api/middleware/error-handler.js
Normal file
13
Docker Backend/src/api/middleware/error-handler.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = (err, req, res, next) => {
|
||||
console.error('Error occurred:', err);
|
||||
|
||||
const statusCode = err.statusCode || 500;
|
||||
const message = err.message || 'Internal Server Error';
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: {
|
||||
message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
}
|
||||
});
|
||||
};
|
||||
53
Docker Backend/src/api/middleware/rate-limiter.js
Normal file
53
Docker Backend/src/api/middleware/rate-limiter.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const config = require('../../config');
|
||||
|
||||
// Simple in-memory rate limiter
|
||||
const rateLimitStore = new Map();
|
||||
|
||||
// Cleanup old entries every minute
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const oneMinute = 60 * 1000;
|
||||
|
||||
for (const [key, data] of rateLimitStore.entries()) {
|
||||
if (now - data.windowStart > oneMinute) {
|
||||
rateLimitStore.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
const sessionId = req.body.sessionId || req.params.sessionId || 'anonymous';
|
||||
const now = Date.now();
|
||||
const oneMinute = 60 * 1000;
|
||||
|
||||
let rateData = rateLimitStore.get(sessionId);
|
||||
|
||||
if (!rateData || now - rateData.windowStart > oneMinute) {
|
||||
// New window
|
||||
rateData = {
|
||||
windowStart: now,
|
||||
requests: []
|
||||
};
|
||||
rateLimitStore.set(sessionId, rateData);
|
||||
}
|
||||
|
||||
// Remove requests older than 1 minute
|
||||
rateData.requests = rateData.requests.filter(timestamp => now - timestamp < oneMinute);
|
||||
|
||||
// Check limit
|
||||
if (rateData.requests.length >= config.preview.rateLimitPerMinute) {
|
||||
const oldestRequest = Math.min(...rateData.requests);
|
||||
const retryAfter = Math.ceil((oneMinute - (now - oldestRequest)) / 1000);
|
||||
|
||||
return res.status(429).json({
|
||||
error: 'Zu viele Vorschau-Anfragen. Bitte warten Sie.',
|
||||
retryAfter,
|
||||
message: `Limit: ${config.preview.rateLimitPerMinute} Anfragen pro Minute`
|
||||
});
|
||||
}
|
||||
|
||||
// Add current request
|
||||
rateData.requests.push(now);
|
||||
|
||||
next();
|
||||
};
|
||||
14
Docker Backend/src/api/middleware/request-logger.js
Normal file
14
Docker Backend/src/api/middleware/request-logger.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
console.log(
|
||||
`[${timestamp}] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
34
Docker Backend/src/api/routes/health-routes.js
Normal file
34
Docker Backend/src/api/routes/health-routes.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const config = require('../../config');
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
scriptalizer: config.scriptalizer.licenseKey ? 'configured' : 'missing',
|
||||
storage: {
|
||||
cache: fs.existsSync(config.paths.cache) && isWritable(config.paths.cache),
|
||||
output: fs.existsSync(config.paths.output) && isWritable(config.paths.output)
|
||||
}
|
||||
};
|
||||
|
||||
const allHealthy = health.scriptalizer === 'configured' &&
|
||||
health.storage.cache &&
|
||||
health.storage.output;
|
||||
|
||||
res.status(allHealthy ? 200 : 503).json(health);
|
||||
});
|
||||
|
||||
function isWritable(path) {
|
||||
try {
|
||||
fs.accessSync(path, fs.constants.W_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
94
Docker Backend/src/api/routes/order-routes.js
Normal file
94
Docker Backend/src/api/routes/order-routes.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const orderController = require('../controllers/order-controller');
|
||||
const { authenticateApiToken } = require('../middleware/auth');
|
||||
|
||||
// Multer konfigurieren für Datei-Uploads (temporär im Speicher)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB max
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Erlaubte Dateitypen
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml', 'application/pdf'];
|
||||
const allowedExts = ['.png', '.jpg', '.jpeg', '.webp', '.svg', '.pdf'];
|
||||
|
||||
const ext = file.originalname.toLowerCase().substring(file.originalname.lastIndexOf('.'));
|
||||
|
||||
if (allowedTypes.includes(file.mimetype) || allowedExts.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Nur PNG, JPG, WEBP, SVG und PDF Dateien sind erlaubt'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/order/finalize
|
||||
* Finalize order by copying cached previews to output directory
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* sessionId: string,
|
||||
* orderNumber: string
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* orderNumber: string,
|
||||
* outputPath: string,
|
||||
* files: string[],
|
||||
* timestamp: string
|
||||
* }
|
||||
*/
|
||||
router.post('/finalize', authenticateApiToken, orderController.finalizeOrder);
|
||||
|
||||
/**
|
||||
* POST /api/order/generate
|
||||
* Generate order from scratch without using cache
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* orderNumber: string,
|
||||
* letters: [
|
||||
* {
|
||||
* text: string,
|
||||
* format: 'a4' | 'a6p' | 'a6l' | 'c6' | 'din_lang',
|
||||
* font: 'tilda' | 'alva' | 'ellie',
|
||||
* type?: 'letter' | 'envelope',
|
||||
* envelopeType?: 'recipient' | 'custom',
|
||||
* placeholders?: { [key: string]: string }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* orderNumber: string,
|
||||
* outputPath: string,
|
||||
* files: string[],
|
||||
* timestamp: string
|
||||
* }
|
||||
*/
|
||||
router.post('/generate', authenticateApiToken, orderController.generateOrder);
|
||||
|
||||
/**
|
||||
* POST /api/order/motif
|
||||
* Upload a motif image for an order
|
||||
*
|
||||
* Request: multipart/form-data
|
||||
* - motif: file (PNG, JPG, WEBP, SVG, PDF)
|
||||
* - orderNumber: string
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* success: boolean,
|
||||
* filename: string,
|
||||
* path: string
|
||||
* }
|
||||
*/
|
||||
router.post('/motif', authenticateApiToken, upload.single('motif'), orderController.uploadMotif);
|
||||
|
||||
module.exports = router;
|
||||
14
Docker Backend/src/api/routes/paypal-routes.js
Normal file
14
Docker Backend/src/api/routes/paypal-routes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const paypalController = require('../controllers/paypal-controller');
|
||||
|
||||
// PayPal-Konfigurationsstatus
|
||||
router.get('/status', paypalController.getStatus);
|
||||
|
||||
// Bestellung erstellen
|
||||
router.post('/orders', paypalController.createOrder);
|
||||
|
||||
// Zahlung erfassen
|
||||
router.post('/orders/:orderID/capture', paypalController.captureOrder);
|
||||
|
||||
module.exports = router;
|
||||
47
Docker Backend/src/api/routes/preview-routes.js
Normal file
47
Docker Backend/src/api/routes/preview-routes.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const previewController = require('../controllers/preview-controller');
|
||||
const { authenticateApiToken } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* POST /api/preview/batch
|
||||
* Generate preview batch (no rate limiting, no batch size limit)
|
||||
* Backend automatically splits into 25-letter batches for Scriptalizer API
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* sessionId: string,
|
||||
* letters: [
|
||||
* {
|
||||
* text: string,
|
||||
* format: 'a4' | 'a6p' | 'a6l' | 'c6' | 'din_lang',
|
||||
* font: 'tilda' | 'alva' | 'ellie',
|
||||
* type?: 'letter' | 'envelope',
|
||||
* envelopeType?: 'recipient' | 'custom',
|
||||
* placeholders?: { [key: string]: string }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* sessionId: string,
|
||||
* files: [
|
||||
* {
|
||||
* index: number,
|
||||
* filename: string,
|
||||
* url: string
|
||||
* }
|
||||
* ],
|
||||
* csvUrl?: string
|
||||
* }
|
||||
*/
|
||||
router.post('/batch', authenticateApiToken, previewController.generateBatch);
|
||||
|
||||
/**
|
||||
* GET /api/preview/:sessionId/:filename
|
||||
* Serve cached preview SVG files
|
||||
*/
|
||||
router.get('/:sessionId/:filename', previewController.servePreview);
|
||||
|
||||
module.exports = router;
|
||||
53
Docker Backend/src/config/index.js
Normal file
53
Docker Backend/src/config/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
require('dotenv').config();
|
||||
const path = require('path');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
module.exports = {
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT, 10) || 4000,
|
||||
|
||||
scriptalizer: {
|
||||
licenseKey: process.env.SCRIPTALIZER_LICENSE_KEY,
|
||||
errFrequency: parseInt(process.env.SCRIPTALIZER_ERR_FREQUENCY, 10) || 0,
|
||||
endpoint: 'https://www.scriptalizer.co.uk/QuantumScriptalize.asmx/Scriptalize',
|
||||
fontMap: {
|
||||
tilda: 'PremiumUltra79',
|
||||
alva: 'PremiumUltra23',
|
||||
ellie: 'PremiumUltra39'
|
||||
},
|
||||
separator: '|||', // Triple pipe separator (tested and working)
|
||||
maxInputSize: 48000 // 48KB limit
|
||||
},
|
||||
|
||||
preview: {
|
||||
// No batch size limit - frontend can send any number of letters
|
||||
// Backend splits into 25-letter batches for Scriptalizer API internally
|
||||
scriptalizerBatchSize: 25
|
||||
// No cache lifetime - files are kept until manually cleaned
|
||||
// No rate limiting
|
||||
},
|
||||
|
||||
paths: {
|
||||
cache: isProduction ? '/app/cache' : path.join(__dirname, '../../cache'),
|
||||
previews: isProduction ? '/app/cache/previews' : path.join(__dirname, '../../cache/previews'),
|
||||
output: isProduction ? '/app/output' : path.join(__dirname, '../../output'),
|
||||
fonts: isProduction ? '/app/fonts' : path.join(__dirname, '../../fonts')
|
||||
},
|
||||
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true
|
||||
},
|
||||
|
||||
auth: {
|
||||
apiToken: process.env.API_TOKEN || null
|
||||
},
|
||||
|
||||
paypal: {
|
||||
clientId: process.env.PAYPAL_CLIENT_ID || '',
|
||||
clientSecret: process.env.PAYPAL_CLIENT_SECRET || '',
|
||||
// 'sandbox' oder 'live'
|
||||
environment: process.env.PAYPAL_ENVIRONMENT || 'sandbox'
|
||||
}
|
||||
};
|
||||
122
Docker Backend/src/lib/page-layout.js
Normal file
122
Docker Backend/src/lib/page-layout.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const PX_PER_MM = 3.78; // 96dpi
|
||||
const FONT_SIZE_PX = 26;
|
||||
|
||||
// Maximale Zeilenanzahl pro Format (für Textvalidierung)
|
||||
const LINE_LIMITS = {
|
||||
a4: 27,
|
||||
a6p: 14,
|
||||
a6l: 9,
|
||||
};
|
||||
|
||||
// Layout Konfiguration pro Format + Orientation
|
||||
const FORMAT_LAYOUTS = {
|
||||
// A4 Hochformat
|
||||
a4: {
|
||||
marginTopMm: 25,
|
||||
marginLeftMm: 20,
|
||||
lineHeightFactor: 1.35,
|
||||
},
|
||||
|
||||
// A6 Hochformat
|
||||
a6p: {
|
||||
marginTopMm: 12,
|
||||
marginLeftMm: 10,
|
||||
lineHeightFactor: 1.3,
|
||||
},
|
||||
|
||||
// A6 Querformat (landscape)
|
||||
a6l: {
|
||||
marginTopMm: 10,
|
||||
marginLeftMm: 8,
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
|
||||
// DIN Lang Kuvert (Querformat)
|
||||
din_lang: {
|
||||
marginTopMm: 50,
|
||||
marginLeftMm: 10, // Links unten in Ecke
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
|
||||
// C6 Kuvert (Querformat)
|
||||
c6: {
|
||||
marginTopMm: 60,
|
||||
marginLeftMm: 10, // Links unten in Ecke
|
||||
lineHeightFactor: 1.2,
|
||||
},
|
||||
};
|
||||
|
||||
// Seiten-Dimensionen
|
||||
const PAGE_FORMATS = {
|
||||
a4: { widthMm: 210, heightMm: 297 },
|
||||
a6p: { widthMm: 105, heightMm: 148 },
|
||||
a6l: { widthMm: 148, heightMm: 105 }, // Querformat: getauscht
|
||||
din_lang: { widthMm: 220, heightMm: 110 },
|
||||
c6: { widthMm: 162, heightMm: 114 },
|
||||
};
|
||||
|
||||
// mm in Pixel umrechnen
|
||||
function mmToPx(mm) {
|
||||
return mm * PX_PER_MM;
|
||||
}
|
||||
|
||||
// Layout für Format holen
|
||||
function getLayoutForFormat(format) {
|
||||
const normalizedFormat = String(format).toLowerCase();
|
||||
const layoutConfig = FORMAT_LAYOUTS[normalizedFormat];
|
||||
|
||||
if (!layoutConfig) {
|
||||
// Fallback auf a4
|
||||
return {
|
||||
marginTopPx: mmToPx(FORMAT_LAYOUTS.a4.marginTopMm),
|
||||
marginLeftPx: mmToPx(FORMAT_LAYOUTS.a4.marginLeftMm),
|
||||
lineHeightPx: FONT_SIZE_PX * FORMAT_LAYOUTS.a4.lineHeightFactor,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
marginTopPx: mmToPx(layoutConfig.marginTopMm),
|
||||
marginLeftPx: mmToPx(layoutConfig.marginLeftMm),
|
||||
lineHeightPx: FONT_SIZE_PX * layoutConfig.lineHeightFactor,
|
||||
};
|
||||
}
|
||||
|
||||
// Seiten-Dimensionen holen
|
||||
function getPageDimensions(format) {
|
||||
const normalizedFormat = String(format).toLowerCase();
|
||||
const dimensions = PAGE_FORMATS[normalizedFormat] || PAGE_FORMATS.a4;
|
||||
|
||||
return {
|
||||
widthMm: dimensions.widthMm,
|
||||
heightMm: dimensions.heightMm,
|
||||
widthPx: mmToPx(dimensions.widthMm),
|
||||
heightPx: mmToPx(dimensions.heightMm),
|
||||
};
|
||||
}
|
||||
|
||||
// XML Escaping
|
||||
function escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// Zeilenlimit für Format holen
|
||||
function getLineLimit(format) {
|
||||
const normalizedFormat = String(format).toLowerCase();
|
||||
return LINE_LIMITS[normalizedFormat] || LINE_LIMITS.a4;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PX_PER_MM,
|
||||
FONT_SIZE_PX,
|
||||
FORMAT_LAYOUTS,
|
||||
PAGE_FORMATS,
|
||||
LINE_LIMITS,
|
||||
mmToPx,
|
||||
getLayoutForFormat,
|
||||
getPageDimensions,
|
||||
getLineLimit,
|
||||
escapeXml
|
||||
};
|
||||
212
Docker Backend/src/lib/svg-font-engine.js
Normal file
212
Docker Backend/src/lib/svg-font-engine.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('../config');
|
||||
|
||||
// Cache je Fontdatei
|
||||
const FONT_CACHE = {};
|
||||
|
||||
// Attribut aus einem Tag lesen (mit Wortgrenze, um z.B. 'd' vs 'id' zu unterscheiden)
|
||||
function getAttr(tag, name) {
|
||||
const re = new RegExp(`\\b${name}="([^"]*)"`);
|
||||
const m = tag.match(re);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// SVG-Font parsen und Metriken plus Glyphen extrahieren
|
||||
function parseSvgFont(fontFileName) {
|
||||
if (FONT_CACHE[fontFileName]) return FONT_CACHE[fontFileName];
|
||||
|
||||
const fontPath = path.join(config.paths.fonts, fontFileName);
|
||||
const svg = fs.readFileSync(fontPath, 'utf8');
|
||||
|
||||
const fontFaceMatch = svg.match(/<font-face[\s\S]*?>/);
|
||||
if (!fontFaceMatch) {
|
||||
throw new Error(`Kein <font-face> in ${fontFileName} gefunden`);
|
||||
}
|
||||
|
||||
const fontFaceTag = fontFaceMatch[0];
|
||||
const unitsPerEm = parseFloat(getAttr(fontFaceTag, 'units-per-em') || '1000');
|
||||
const ascent = parseFloat(getAttr(fontFaceTag, 'ascent') || '0');
|
||||
const descent = parseFloat(getAttr(fontFaceTag, 'descent') || '0');
|
||||
|
||||
const glyphRegex = /<glyph[\s\S]*?\/>/g;
|
||||
const glyphs = {};
|
||||
let m;
|
||||
|
||||
while ((m = glyphRegex.exec(svg)) !== null) {
|
||||
const tag = m[0];
|
||||
const unicode = getAttr(tag, 'unicode');
|
||||
const d = getAttr(tag, 'd');
|
||||
const adv = parseFloat(getAttr(tag, 'horiz-adv-x') || '0');
|
||||
|
||||
if (!unicode || !d) continue;
|
||||
|
||||
glyphs[unicode] = {
|
||||
d,
|
||||
adv,
|
||||
};
|
||||
}
|
||||
|
||||
const fontData = {
|
||||
unitsPerEm,
|
||||
ascent,
|
||||
descent,
|
||||
glyphs,
|
||||
};
|
||||
|
||||
FONT_CACHE[fontFileName] = fontData;
|
||||
return fontData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breite einer Textzeile in Pixeln messen.
|
||||
*/
|
||||
function measureTextWidthPx({ text, fontFileName, fontSizePx }) {
|
||||
const { unitsPerEm, glyphs } = parseSvgFont(fontFileName);
|
||||
const scale = fontSizePx / unitsPerEm;
|
||||
|
||||
let x = 0;
|
||||
|
||||
for (const ch of String(text || '')) {
|
||||
const glyph = glyphs[ch];
|
||||
|
||||
if (!glyph) {
|
||||
// Fallback für Leerzeichen / unbekannte Zeichen
|
||||
const spacePx = fontSizePx * 0.4;
|
||||
x += spacePx;
|
||||
continue;
|
||||
}
|
||||
|
||||
const advPx = (glyph.adv || unitsPerEm * 0.5) * scale;
|
||||
x += advPx;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text nach verfügbarer Breite umbrechen.
|
||||
* Garantiert: keine Zeile wird breiter als maxWidthPx.
|
||||
*/
|
||||
function wrapTextToLinesByWidth({
|
||||
text,
|
||||
maxWidthPx,
|
||||
fontFileName,
|
||||
fontSizePx,
|
||||
}) {
|
||||
const paragraphs = String(text || '').split(/\r?\n/);
|
||||
const allLines = [];
|
||||
|
||||
const measure = (t) =>
|
||||
measureTextWidthPx({ text: t, fontFileName, fontSizePx });
|
||||
|
||||
for (const para of paragraphs) {
|
||||
const words = para.split(/\s+/).filter(Boolean);
|
||||
if (words.length === 0) {
|
||||
allLines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentLine = '';
|
||||
|
||||
for (const word of words) {
|
||||
// Wort alleine zu breit → Zeichenweise brechen
|
||||
if (measure(word) > maxWidthPx) {
|
||||
if (currentLine) {
|
||||
allLines.push(currentLine);
|
||||
currentLine = '';
|
||||
}
|
||||
|
||||
let buf = '';
|
||||
for (const ch of word) {
|
||||
const candidate = buf + ch;
|
||||
if (measure(candidate) > maxWidthPx && buf) {
|
||||
allLines.push(buf);
|
||||
buf = ch;
|
||||
} else {
|
||||
buf = candidate;
|
||||
}
|
||||
}
|
||||
if (buf) currentLine = buf;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentLine) {
|
||||
currentLine = word;
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = `${currentLine} ${word}`;
|
||||
if (measure(candidate) <= maxWidthPx) {
|
||||
currentLine = candidate;
|
||||
} else {
|
||||
allLines.push(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
allLines.push(currentLine);
|
||||
}
|
||||
}
|
||||
|
||||
return allLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Textzeilen in Pfade mit Transform-Matrix umwandeln.
|
||||
*/
|
||||
function layoutTextToPaths({
|
||||
lines,
|
||||
fontFileName,
|
||||
fontSizePx,
|
||||
startX,
|
||||
startY,
|
||||
lineHeightPx,
|
||||
}) {
|
||||
const { unitsPerEm, glyphs } = parseSvgFont(fontFileName);
|
||||
|
||||
const scale = fontSizePx / unitsPerEm;
|
||||
|
||||
const resultPaths = [];
|
||||
let baselineY = startY;
|
||||
|
||||
for (const line of lines) {
|
||||
let x = startX;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
const glyph = glyphs[ch];
|
||||
|
||||
if (!glyph) {
|
||||
// Leerzeichen mit festem Abstand
|
||||
const spacePx = fontSizePx * 0.4;
|
||||
x += spacePx;
|
||||
continue;
|
||||
}
|
||||
|
||||
const d = glyph.d;
|
||||
const advPx = (glyph.adv || unitsPerEm * 0.5) * scale;
|
||||
|
||||
// Einfache Transform-Matrix ohne Rotation
|
||||
const transform = `matrix(${scale},0,0,${-scale},${x},${baselineY})`;
|
||||
|
||||
resultPaths.push({
|
||||
d,
|
||||
transform,
|
||||
});
|
||||
|
||||
x += advPx;
|
||||
}
|
||||
|
||||
baselineY += lineHeightPx;
|
||||
}
|
||||
|
||||
return resultPaths;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
measureTextWidthPx,
|
||||
wrapTextToLinesByWidth,
|
||||
layoutTextToPaths
|
||||
};
|
||||
213
Docker Backend/src/lib/svg-generator.js
Normal file
213
Docker Backend/src/lib/svg-generator.js
Normal file
@@ -0,0 +1,213 @@
|
||||
const {
|
||||
FONT_SIZE_PX,
|
||||
getLayoutForFormat,
|
||||
getPageDimensions,
|
||||
getLineLimit,
|
||||
escapeXml
|
||||
} = require('./page-layout');
|
||||
|
||||
const {
|
||||
layoutTextToPaths,
|
||||
wrapTextToLinesByWidth,
|
||||
measureTextWidthPx
|
||||
} = require('./svg-font-engine');
|
||||
|
||||
// Mapping der Handschriften auf SVG-Font-Dateien
|
||||
const SVG_FONT_CONFIG = {
|
||||
tilda: { file: 'tilda.svg' },
|
||||
alva: { file: 'alva.svg' },
|
||||
ellie: { file: 'ellie.svg' },
|
||||
};
|
||||
|
||||
function normalizeFontKey(font) {
|
||||
if (!font) return 'tilda';
|
||||
const f = String(font).toLowerCase();
|
||||
if (['tilda', 'alva', 'ellie'].includes(f)) return f;
|
||||
return 'tilda';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Schriftstück (Brief/Postkarte) als SVG
|
||||
* @param {string} text - Der scriptalisierte Text
|
||||
* @param {string} format - Format: a4, a6p, a6l
|
||||
* @param {object} options - { font: 'tilda', alignment: 'left' }
|
||||
*/
|
||||
function generateLetterSVG(text, format, options = {}) {
|
||||
const fontKey = normalizeFontKey(options.font);
|
||||
const fontConfig = SVG_FONT_CONFIG[fontKey];
|
||||
|
||||
const { widthPx, heightPx, widthMm, heightMm } = getPageDimensions(format);
|
||||
const { marginTopPx, marginLeftPx, lineHeightPx } = getLayoutForFormat(format);
|
||||
|
||||
// Automatischer Zeilenumbruch
|
||||
const maxWidthPx = widthPx - 2 * marginLeftPx;
|
||||
|
||||
const lines = wrapTextToLinesByWidth({
|
||||
text: String(text || ''),
|
||||
maxWidthPx,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
});
|
||||
|
||||
const firstBaselineY = marginTopPx + FONT_SIZE_PX;
|
||||
|
||||
const paths = layoutTextToPaths({
|
||||
lines,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
startX: marginLeftPx,
|
||||
startY: firstBaselineY,
|
||||
lineHeightPx,
|
||||
});
|
||||
|
||||
const pathElements = paths
|
||||
.map(
|
||||
(p) => `<path d="${escapeXml(p.d)}"
|
||||
transform="${p.transform}"
|
||||
stroke="#000000"
|
||||
stroke-width="0.6"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>`
|
||||
)
|
||||
.join('\n ');
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="${widthMm}mm"
|
||||
height="${heightMm}mm"
|
||||
viewBox="0 0 ${widthPx} ${heightPx}">
|
||||
<rect x="0" y="0" width="${widthPx}" height="${heightPx}" fill="#FFFFFF" />
|
||||
${pathElements}
|
||||
</svg>`;
|
||||
|
||||
const lineLimit = getLineLimit(format);
|
||||
const lineCount = lines.length;
|
||||
|
||||
return {
|
||||
svg: svg.trim(),
|
||||
lineCount,
|
||||
lineLimit,
|
||||
overflow: lineCount > lineLimit
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen Umschlag als SVG
|
||||
* @param {string} text - Der scriptalisierte Text
|
||||
* @param {string} format - Format: c6, din_lang
|
||||
* @param {string} type - Typ: 'recipient' (links), 'free' (links, freie Adresse), 'custom' (mittig)
|
||||
* @param {object} options - { font: 'tilda' }
|
||||
*/
|
||||
function generateEnvelopeSVG(text, format, type, options = {}) {
|
||||
const fontKey = normalizeFontKey(options.font);
|
||||
const fontConfig = SVG_FONT_CONFIG[fontKey];
|
||||
|
||||
const { widthPx, heightPx, widthMm, heightMm } = getPageDimensions(format);
|
||||
const { marginTopPx, marginLeftPx, lineHeightPx } = getLayoutForFormat(format);
|
||||
|
||||
let lines;
|
||||
let startX;
|
||||
let startY;
|
||||
|
||||
if (type === 'custom') {
|
||||
// Mittig zentriert, 70% der Umschlagsbreite
|
||||
const maxWidthPx = widthPx * 0.7;
|
||||
|
||||
lines = wrapTextToLinesByWidth({
|
||||
text: String(text || ''),
|
||||
maxWidthPx,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
});
|
||||
|
||||
// Zentrierung berechnen
|
||||
const totalTextHeight = lines.length * lineHeightPx;
|
||||
startY = (heightPx - totalTextHeight) / 2 + FONT_SIZE_PX;
|
||||
|
||||
// Jede Zeile einzeln zentrieren
|
||||
// startX wird später pro Zeile berechnet
|
||||
startX = 0; // Dummy-Wert, wird für jede Zeile überschrieben
|
||||
|
||||
} else {
|
||||
// type === 'recipient': Links unten in Ecke
|
||||
const maxWidthPx = widthPx - marginLeftPx - 20;
|
||||
|
||||
lines = wrapTextToLinesByWidth({
|
||||
text: String(text || ''),
|
||||
maxWidthPx,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
});
|
||||
|
||||
startX = marginLeftPx;
|
||||
startY = marginTopPx + FONT_SIZE_PX;
|
||||
}
|
||||
|
||||
let paths = [];
|
||||
|
||||
if (type === 'custom') {
|
||||
// Bei custom type: Jede Zeile einzeln zentrieren
|
||||
let currentY = startY;
|
||||
for (const line of lines) {
|
||||
const lineWidth = measureTextWidthPx({
|
||||
text: line,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX
|
||||
});
|
||||
const centeredX = (widthPx - lineWidth) / 2;
|
||||
|
||||
const linePaths = layoutTextToPaths({
|
||||
lines: [line],
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
startX: centeredX,
|
||||
startY: currentY,
|
||||
lineHeightPx,
|
||||
});
|
||||
|
||||
paths.push(...linePaths);
|
||||
currentY += lineHeightPx;
|
||||
}
|
||||
} else {
|
||||
// Bei recipient type: Normal links ausrichten
|
||||
paths = layoutTextToPaths({
|
||||
lines,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: FONT_SIZE_PX,
|
||||
startX,
|
||||
startY,
|
||||
lineHeightPx,
|
||||
});
|
||||
}
|
||||
|
||||
const pathElements = paths
|
||||
.map(
|
||||
(p) => `<path d="${escapeXml(p.d)}"
|
||||
transform="${p.transform}"
|
||||
stroke="#000000"
|
||||
stroke-width="0.6"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>`
|
||||
)
|
||||
.join('\n ');
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="${widthMm}mm"
|
||||
height="${heightMm}mm"
|
||||
viewBox="0 0 ${widthPx} ${heightPx}">
|
||||
<rect x="0" y="0" width="${widthPx}" height="${heightPx}" fill="#FFFFFF" />
|
||||
${pathElements}
|
||||
</svg>`;
|
||||
|
||||
return svg.trim();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateLetterSVG,
|
||||
generateEnvelopeSVG
|
||||
};
|
||||
55
Docker Backend/src/server.js
Normal file
55
Docker Backend/src/server.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const config = require('./config');
|
||||
|
||||
const previewRoutes = require('./api/routes/preview-routes');
|
||||
const orderRoutes = require('./api/routes/order-routes');
|
||||
const healthRoutes = require('./api/routes/health-routes');
|
||||
const paypalRoutes = require('./api/routes/paypal-routes');
|
||||
|
||||
const errorHandler = require('./api/middleware/error-handler');
|
||||
const requestLogger = require('./api/middleware/request-logger');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(cors(config.cors));
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(requestLogger);
|
||||
|
||||
// Routes
|
||||
app.use('/health', healthRoutes);
|
||||
app.use('/api/preview', previewRoutes);
|
||||
app.use('/api/order', orderRoutes);
|
||||
app.use('/api/paypal', paypalRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(errorHandler);
|
||||
|
||||
// Start server
|
||||
app.listen(config.port, () => {
|
||||
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Skrift Backend Server ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝');
|
||||
console.log(`Environment: ${config.env}`);
|
||||
console.log(`Port: ${config.port}`);
|
||||
console.log(`Batch Size: Unlimited (auto-split to ${config.preview.scriptalizerBatchSize} for API)`);
|
||||
console.log(`Cache: Disabled`);
|
||||
console.log(`Rate Limit: Disabled`);
|
||||
console.log(`Scriptalizer: ${config.scriptalizer.licenseKey ? 'Configured ✓' : 'Missing ✗'}`);
|
||||
console.log('════════════════════════════════════════════════════════════');
|
||||
console.log(`Server running at http://localhost:${config.port}`);
|
||||
console.log('════════════════════════════════════════════════════════════\n');
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nSIGINT received, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
283
Docker Backend/src/services/placeholder-service.js
Normal file
283
Docker Backend/src/services/placeholder-service.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Ersetzt Platzhalter im Text
|
||||
* @param {string} text - Text mit Platzhaltern wie [[Vorname]]
|
||||
* @param {object} placeholders - Objekt mit Platzhalter-Werten {Vorname: 'Max', ...}
|
||||
* @returns {string} - Text mit ersetzten Platzhaltern
|
||||
*/
|
||||
function replacePlaceholders(text, placeholders) {
|
||||
if (!placeholders || typeof placeholders !== 'object') {
|
||||
return text;
|
||||
}
|
||||
|
||||
let result = text;
|
||||
|
||||
for (const [key, value] of Object.entries(placeholders)) {
|
||||
const placeholder = `[[${key}]]`;
|
||||
// Case-insensitive replacement: 'i' flag
|
||||
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
||||
result = result.replace(regex, value || '');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped einen Wert für CSV
|
||||
*/
|
||||
function escapeCSV(value) {
|
||||
const str = String(value || '');
|
||||
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV aus Platzhalter-Daten (für Briefe)
|
||||
* @param {Array} letters - Array von Letter-Objekten mit placeholders
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generatePlaceholderCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Briefe filtern (keine Umschläge)
|
||||
const letterItems = letters.filter(l => l.type !== 'envelope');
|
||||
if (letterItems.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sammle alle unique Keys
|
||||
const allKeys = new Set();
|
||||
letterItems.forEach(letter => {
|
||||
if (letter.placeholders) {
|
||||
Object.keys(letter.placeholders).forEach(key => allKeys.add(key));
|
||||
}
|
||||
});
|
||||
|
||||
if (allKeys.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['BriefNr', ...Array.from(allKeys).sort()];
|
||||
|
||||
// Header
|
||||
const header = keys.join(',');
|
||||
|
||||
// Rows
|
||||
const rows = letterItems.map((letter, index) => {
|
||||
const briefNr = String(letter.index !== undefined ? letter.index : index).padStart(3, '0');
|
||||
const values = [briefNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = letter.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für Empfängerdaten (Umschläge im recipientData-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateRecipientCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im recipient-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'recipient');
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Prüfe ob Empfängerdaten vorhanden sind (vorname, name, strasse, etc.)
|
||||
const recipientKeys = ['vorname', 'name', 'strasse', 'hausnummer', 'plz', 'ort', 'land'];
|
||||
const hasRecipientData = envelopes.some(env =>
|
||||
env.placeholders && recipientKeys.some(key => env.placeholders[key])
|
||||
);
|
||||
|
||||
if (!hasRecipientData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['UmschlagNr', ...recipientKeys];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = envelope.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für freie Adressen (Umschläge im free-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateFreeAddressCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im free-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'free');
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Freie Adressen haben bis zu 5 Zeilen
|
||||
const keys = ['UmschlagNr', 'Zeile1', 'Zeile2', 'Zeile3', 'Zeile4', 'Zeile5'];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
|
||||
// Text in Zeilen aufteilen
|
||||
const textLines = (envelope.text || '').split('\n');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
values.push(escapeCSV(textLines[i] || ''));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für Umschlag-Platzhalter (Umschläge im customText-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateEnvelopePlaceholderCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
console.log('[CSV] generateEnvelopePlaceholderCSV: No letters');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im custom-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'custom');
|
||||
console.log('[CSV] Found custom envelopes:', envelopes.length);
|
||||
console.log('[CSV] Envelope details:', envelopes.map(e => ({ type: e.type, envelopeType: e.envelopeType, placeholders: e.placeholders })));
|
||||
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sammle alle unique Keys (außer Standard-Empfängerfelder)
|
||||
const recipientKeys = ['vorname', 'name', 'strasse', 'hausnummer', 'plz', 'ort', 'land'];
|
||||
const allKeys = new Set();
|
||||
envelopes.forEach(envelope => {
|
||||
if (envelope.placeholders) {
|
||||
console.log('[CSV] Envelope placeholders keys:', Object.keys(envelope.placeholders));
|
||||
Object.keys(envelope.placeholders).forEach(key => {
|
||||
// Nur nicht-Empfängerfelder sammeln
|
||||
if (!recipientKeys.includes(key.toLowerCase())) {
|
||||
allKeys.add(key);
|
||||
console.log('[CSV] Added key:', key);
|
||||
} else {
|
||||
console.log('[CSV] Skipped recipient key:', key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[CSV] All collected keys:', Array.from(allKeys));
|
||||
|
||||
if (allKeys.size === 0) {
|
||||
console.log('[CSV] No custom placeholder keys found for envelopes');
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['UmschlagNr', ...Array.from(allKeys).sort()];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = envelope.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert alle CSV-Dateien für eine Bestellung
|
||||
* @param {Array} letters - Array von Letter-Objekten (Briefe und Umschläge)
|
||||
* @returns {Object} - Objekt mit CSV-Inhalten { placeholders, recipients, envelopePlaceholders, freeAddresses }
|
||||
*/
|
||||
function generateAllCSVs(letters) {
|
||||
const result = {
|
||||
placeholders: null, // Brief-Platzhalter
|
||||
recipients: null, // Empfängerdaten für Umschläge (recipientData-Modus)
|
||||
envelopePlaceholders: null, // Umschlag-Platzhalter (customText-Modus)
|
||||
freeAddresses: null // Freie Adressen für Umschläge (free-Modus)
|
||||
};
|
||||
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Brief-Platzhalter CSV
|
||||
const placeholderCSV = generatePlaceholderCSV(letters);
|
||||
if (placeholderCSV) {
|
||||
result.placeholders = placeholderCSV;
|
||||
}
|
||||
|
||||
// Empfänger CSV (für recipientData-Modus)
|
||||
const recipientCSV = generateRecipientCSV(letters);
|
||||
if (recipientCSV) {
|
||||
result.recipients = recipientCSV;
|
||||
}
|
||||
|
||||
// Umschlag-Platzhalter CSV (für customText-Modus)
|
||||
const envelopePlaceholderCSV = generateEnvelopePlaceholderCSV(letters);
|
||||
if (envelopePlaceholderCSV) {
|
||||
result.envelopePlaceholders = envelopePlaceholderCSV;
|
||||
}
|
||||
|
||||
// Freie Adressen CSV (für free-Modus)
|
||||
const freeAddressCSV = generateFreeAddressCSV(letters);
|
||||
if (freeAddressCSV) {
|
||||
result.freeAddresses = freeAddressCSV;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
replacePlaceholders,
|
||||
generatePlaceholderCSV,
|
||||
generateRecipientCSV,
|
||||
generateEnvelopePlaceholderCSV,
|
||||
generateFreeAddressCSV,
|
||||
generateAllCSVs,
|
||||
escapeCSV
|
||||
};
|
||||
230
Docker Backend/src/services/scriptalizer-service.js
Normal file
230
Docker Backend/src/services/scriptalizer-service.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const https = require("https");
|
||||
const { parseStringPromise } = require("xml2js");
|
||||
const config = require("../config");
|
||||
|
||||
// API Request Counter (resets daily)
|
||||
const stats = {
|
||||
requestsToday: 0,
|
||||
textsToday: 0,
|
||||
lastResetDate: new Date().toDateString(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Prüft ob der Zähler zurückgesetzt werden muss (neuer Tag)
|
||||
*/
|
||||
function checkAndResetDailyStats() {
|
||||
const today = new Date().toDateString();
|
||||
if (stats.lastResetDate !== today) {
|
||||
console.log(`[Scriptalizer] 📅 New day detected, resetting counters`);
|
||||
console.log(`[Scriptalizer] 📊 Yesterday's stats: ${stats.requestsToday} API calls, ${stats.textsToday} texts processed`);
|
||||
stats.requestsToday = 0;
|
||||
stats.textsToday = 0;
|
||||
stats.lastResetDate = today;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Stats zurück
|
||||
*/
|
||||
function getStats() {
|
||||
checkAndResetDailyStats();
|
||||
return { ...stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die Scriptalizer API auf
|
||||
* @param {string} inputText - Text zum Scriptalisieren
|
||||
* @param {string} fontName - Font-Name (z.B. 'PremiumUltra79')
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<{status: string, outputText: string}>}
|
||||
*/
|
||||
async function callScriptalizer(inputText, fontName, errFrequency = 10) {
|
||||
// Prüfe ob neuer Tag
|
||||
checkAndResetDailyStats();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!config.scriptalizer.licenseKey) {
|
||||
reject(new Error("Scriptalizer License Key fehlt in Konfiguration"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use x-www-form-urlencoded format
|
||||
const params = new URLSearchParams();
|
||||
params.append("LicenseKey", config.scriptalizer.licenseKey);
|
||||
params.append("FontName", fontName);
|
||||
params.append("ErrFrequency", String(errFrequency));
|
||||
params.append("InputText", inputText);
|
||||
|
||||
const body = params.toString();
|
||||
|
||||
// Check input size (48KB limit)
|
||||
if (Buffer.byteLength(body) > config.scriptalizer.maxInputSize) {
|
||||
reject(
|
||||
new Error(
|
||||
`Input size exceeds 48KB limit (${Buffer.byteLength(body)} bytes)`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
hostname: "www.scriptalizer.co.uk",
|
||||
port: 443,
|
||||
path: "/QuantumScriptalize.asmx/Scriptalize",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
},
|
||||
timeout: 30000, // 30 second timeout
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = "";
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on("end", async () => {
|
||||
try {
|
||||
const result = await parseStringPromise(data, {
|
||||
explicitArray: false,
|
||||
});
|
||||
const response = result.ScriptalizerResponse;
|
||||
|
||||
if (!response || response.Status !== "OK") {
|
||||
reject(new Error(response?.Status || "UNKNOWN_STATUS"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Zähler erhöhen bei erfolgreichem Request
|
||||
stats.requestsToday++;
|
||||
console.log(`[Scriptalizer] 📊 API Request #${stats.requestsToday} today completed`);
|
||||
|
||||
resolve({ status: "OK", outputText: response.OutputText });
|
||||
} catch (err) {
|
||||
reject(
|
||||
new Error(`Failed to parse Scriptalizer response: ${err.message}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
reject(new Error(`Scriptalizer request failed: ${err.message}`));
|
||||
});
|
||||
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("Scriptalizer request timed out"));
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scriptalisiert mehrere Texte in 25er Batches
|
||||
* @param {string[]} texts - Array von Texten
|
||||
* @param {string} font - Font-Key (tilda, alva, ellie)
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<string[]>} - Array von scriptalisierten Texten
|
||||
*/
|
||||
async function scriptalizeBatch(texts, font = "tilda", errFrequency = 10) {
|
||||
if (!Array.isArray(texts) || texts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fontName =
|
||||
config.scriptalizer.fontMap[font.toLowerCase()] ||
|
||||
config.scriptalizer.fontMap.tilda;
|
||||
const separator = config.scriptalizer.separator;
|
||||
const BATCH_SIZE = 25; // 25 Texte pro Scriptalizer API Call
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] Processing ${texts.length} texts in batches of ${BATCH_SIZE}`
|
||||
);
|
||||
|
||||
const allScriptalized = [];
|
||||
|
||||
// Teile in 25er Batches auf
|
||||
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
||||
const batch = texts.slice(i, i + BATCH_SIZE);
|
||||
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
|
||||
const totalBatches = Math.ceil(texts.length / BATCH_SIZE);
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] Processing batch ${batchNumber}/${totalBatches} (${
|
||||
batch.length
|
||||
} texts, indices ${i}-${i + batch.length - 1})`
|
||||
);
|
||||
|
||||
// Kombiniere Texte mit Separator
|
||||
const combinedText = batch.join(separator);
|
||||
|
||||
try {
|
||||
const result = await callScriptalizer(
|
||||
combinedText,
|
||||
fontName,
|
||||
errFrequency
|
||||
);
|
||||
|
||||
// Trenne Ergebnis wieder
|
||||
const scriptalized = result.outputText.split(separator);
|
||||
|
||||
if (scriptalized.length !== batch.length) {
|
||||
console.warn(
|
||||
`[Scriptalizer] Batch ${batchNumber}: returned ${scriptalized.length} parts, expected ${batch.length}`
|
||||
);
|
||||
}
|
||||
|
||||
allScriptalized.push(...scriptalized);
|
||||
console.log(
|
||||
`[Scriptalizer] Batch ${batchNumber}/${totalBatches} completed successfully`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`[Scriptalizer] Batch ${batchNumber} failed:`, err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Textzähler erhöhen
|
||||
stats.textsToday += allScriptalized.length;
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] All batches completed. Total scriptalized: ${allScriptalized.length}`
|
||||
);
|
||||
console.log(
|
||||
`[Scriptalizer] 📊 Daily stats: ${stats.requestsToday} API calls, ${stats.textsToday} texts processed`
|
||||
);
|
||||
return allScriptalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scriptalisiert einen einzelnen Text
|
||||
* @param {string} text - Text zum Scriptalisieren
|
||||
* @param {string} font - Font-Key (tilda, alva, ellie)
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<string>} - Scriptalisierter Text
|
||||
*/
|
||||
async function scriptalizeSingle(text, font = "tilda", errFrequency = 10) {
|
||||
const fontName =
|
||||
config.scriptalizer.fontMap[font.toLowerCase()] ||
|
||||
config.scriptalizer.fontMap.tilda;
|
||||
|
||||
try {
|
||||
const result = await callScriptalizer(text, fontName, errFrequency);
|
||||
return result.outputText;
|
||||
} catch (err) {
|
||||
console.error("Scriptalizer single call failed:", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scriptalizeBatch,
|
||||
scriptalizeSingle,
|
||||
getStats,
|
||||
};
|
||||
58
Docker Backend/test-9-orders.json
Normal file
58
Docker Backend/test-9-orders.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-TILDA-A4",
|
||||
"font": "tilda",
|
||||
"format": "a4",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-TILDA-A6Q",
|
||||
"font": "tilda",
|
||||
"format": "a6l",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-TILDA-A6H",
|
||||
"font": "tilda",
|
||||
"format": "a6p",
|
||||
"envelopeType": "custom"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ALVA-A4",
|
||||
"font": "alva",
|
||||
"format": "a4",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ALVA-A6Q",
|
||||
"font": "alva",
|
||||
"format": "a6l",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ALVA-A6H",
|
||||
"font": "alva",
|
||||
"format": "a6p",
|
||||
"envelopeType": "custom"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ELLIE-A4",
|
||||
"font": "ellie",
|
||||
"format": "a4",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ELLIE-A6Q",
|
||||
"font": "ellie",
|
||||
"format": "a6l",
|
||||
"envelopeType": "recipient"
|
||||
},
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-ELLIE-A6H",
|
||||
"font": "ellie",
|
||||
"format": "a6p",
|
||||
"envelopeType": "custom"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
Docker Backend/test-api.sh
Normal file
39
Docker Backend/test-api.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test Skrift Backend API
|
||||
|
||||
BASE_URL="http://localhost:4000"
|
||||
SESSION_ID="test-$(date +%s)"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Testing Skrift Backend API"
|
||||
echo "=========================================="
|
||||
|
||||
echo -e "\n1. Health Check..."
|
||||
curl -s "$BASE_URL/health" | python -m json.tool
|
||||
|
||||
echo -e "\n\n2. Preview Batch (Single Letter)..."
|
||||
curl -s -X POST "$BASE_URL/api/preview/batch" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"sessionId": "'"$SESSION_ID"'",
|
||||
"batchIndex": 0,
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"text": "Hallo [[Vorname]] [[Nachname]],\n\ndein persönlicher Gutscheincode lautet: [[Gutscheincode]]\n\nViele Grüße",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Gutscheincode": "SAVE20"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": []
|
||||
}
|
||||
}' | python -m json.tool
|
||||
|
||||
echo -e "\n\nDone! Session ID: $SESSION_ID"
|
||||
85
Docker Backend/test-complete.json
Normal file
85
Docker Backend/test-complete.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"sessionId": "test-complete-workflow",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nhiermit bestätigen wir Ihre Bestellung mit der Nummer [[Bestellnummer]].\n\nIhr persönlicher Gutscheincode lautet: [[Gutscheincode]]\n\nEr ist gültig bis zum [[Ablaufdatum]].\n\nVielen Dank für Ihr Vertrauen!\n\nMit freundlichen Grüßen\nIhr Skrift-Team",
|
||||
"placeholders": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Bestellnummer": "SK-2026-001",
|
||||
"Gutscheincode": "SAVE20",
|
||||
"Ablaufdatum": "31.12.2026",
|
||||
"Strasse": "Hauptstr. 1",
|
||||
"PLZ": "10115",
|
||||
"Ort": "Berlin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "a6p",
|
||||
"font": "alva",
|
||||
"text": "Liebe/r [[Vorname]],\n\nvielen Dank für deine Bestellung!\n\nDein Code: [[Gutscheincode]]\n\nHerzliche Grüße",
|
||||
"placeholders": {
|
||||
"Vorname": "Anna",
|
||||
"Nachname": "Schmidt",
|
||||
"Gutscheincode": "WINTER50",
|
||||
"Strasse": "Bahnhofstr. 5",
|
||||
"PLZ": "80331",
|
||||
"Ort": "München"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"format": "a6l",
|
||||
"font": "ellie",
|
||||
"text": "Alles Gute zum Geburtstag, [[Vorname]]!\n\nWir wünschen dir einen wundervollen Tag!",
|
||||
"placeholders": {
|
||||
"Vorname": "Julia",
|
||||
"Nachname": "Weber",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"font": "tilda",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Max",
|
||||
"Nachname": "Mustermann",
|
||||
"Strasse": "Hauptstr. 1",
|
||||
"PLZ": "10115",
|
||||
"Ort": "Berlin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "c6",
|
||||
"font": "alva",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Anna",
|
||||
"Nachname": "Schmidt",
|
||||
"Strasse": "Bahnhofstr. 5",
|
||||
"PLZ": "80331",
|
||||
"Ort": "München"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"format": "din_lang",
|
||||
"font": "ellie",
|
||||
"type": "custom",
|
||||
"data": {
|
||||
"customText": "Für meine liebe Freundin Julia"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
Docker Backend/test-finalize.json
Normal file
4
Docker Backend/test-finalize.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sessionId": "test-complete-workflow",
|
||||
"orderNumber": "SK-2026-01-02-001"
|
||||
}
|
||||
36
Docker Backend/test-generate-with-envelopes.json
Normal file
36
Docker Backend/test-generate-with-envelopes.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-002",
|
||||
"config": {
|
||||
"font": "tilda",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nvielen Dank für Ihre Bestellung!\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"font": "tilda",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
52
Docker Backend/test-order-direct.json
Normal file
52
Docker Backend/test-order-direct.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"orderNumber": "SK-2026-01-02-003",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte/r [[Vorname]] [[Nachname]],\n\nvielen Dank für Ihre Bestellung!\n\nMit freundlichen Grüßen",
|
||||
"placeholders": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "a6p",
|
||||
"font": "alva",
|
||||
"text": "Hallo [[Vorname]]!\n\nSchöne Grüße!",
|
||||
"placeholders": {
|
||||
"Vorname": "Sarah",
|
||||
"Nachname": "Fischer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"envelopes": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "c6",
|
||||
"font": "tilda",
|
||||
"type": "recipient",
|
||||
"data": {
|
||||
"Vorname": "Thomas",
|
||||
"Nachname": "Müller",
|
||||
"Strasse": "Lindenweg 12",
|
||||
"PLZ": "50667",
|
||||
"Ort": "Köln"
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"format": "din_lang",
|
||||
"font": "alva",
|
||||
"type": "custom",
|
||||
"data": {
|
||||
"customText": "Für Sarah - Alles Gute!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
12
Docker Backend/test-request.json
Normal file
12
Docker Backend/test-request.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"sessionId": "test-simple",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Hallo Max Mustermann,\n\ndein persönlicher Gutscheincode lautet: SAVE20\n\nViele Grüße",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
127
Docker Backend/test-scriptalizer-direct.js
Normal file
127
Docker Backend/test-scriptalizer-direct.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Zeigt den exakten Scriptalizer API-Call für Tilda Font
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const LICENSE_KEY = 'f9918b40-d11c-11f0-b558-0800200c9a66';
|
||||
const FONT_NAME = 'PremiumUltra79'; // Tilda
|
||||
const ERR_FREQUENCY = 0;
|
||||
const INPUT_TEXT = 'Sehr geehrte Damen und Herren,\n\nhiermit bestätigen wir Ihre Bestellung.\n\nMit freundlichen Grüßen\nIhr Skrift-Team';
|
||||
|
||||
// Build request body (x-www-form-urlencoded)
|
||||
const params = new URLSearchParams();
|
||||
params.append('LicenseKey', LICENSE_KEY);
|
||||
params.append('FontName', FONT_NAME);
|
||||
params.append('ErrFrequency', String(ERR_FREQUENCY));
|
||||
params.append('InputText', INPUT_TEXT);
|
||||
|
||||
const body = params.toString();
|
||||
|
||||
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ SCRIPTALIZER API CALL - TILDA FONT ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
console.log('📍 ENDPOINT:');
|
||||
console.log('https://www.scriptalizer.co.uk/QuantumScriptalize.asmx/Scriptalize\n');
|
||||
|
||||
console.log('📋 METHOD:');
|
||||
console.log('POST\n');
|
||||
|
||||
console.log('📦 HEADERS:');
|
||||
console.log({
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
});
|
||||
console.log('');
|
||||
|
||||
console.log('📝 REQUEST BODY (x-www-form-urlencoded):');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(body);
|
||||
console.log('─'.repeat(60));
|
||||
console.log('');
|
||||
|
||||
console.log('📊 DECODED PARAMETERS:');
|
||||
console.log({
|
||||
LicenseKey: LICENSE_KEY,
|
||||
FontName: FONT_NAME,
|
||||
ErrFrequency: ERR_FREQUENCY,
|
||||
InputText: INPUT_TEXT.substring(0, 100) + '...'
|
||||
});
|
||||
console.log('');
|
||||
|
||||
console.log('🔧 CURL COMMAND:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`curl -X POST 'https://www.scriptalizer.co.uk/QuantumScriptalize.asmx/Scriptalize' \\
|
||||
-H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \\
|
||||
-d 'LicenseKey=${LICENSE_KEY}' \\
|
||||
-d 'FontName=${FONT_NAME}' \\
|
||||
-d 'ErrFrequency=${ERR_FREQUENCY}' \\
|
||||
-d 'InputText=${INPUT_TEXT.replace(/\n/g, '\\n')}'`);
|
||||
console.log('─'.repeat(60));
|
||||
console.log('');
|
||||
|
||||
console.log('📡 SENDING REQUEST TO SCRIPTALIZER...\n');
|
||||
|
||||
const options = {
|
||||
hostname: 'www.scriptalizer.co.uk',
|
||||
port: 443,
|
||||
path: '/QuantumScriptalize.asmx/Scriptalize',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
console.log(`📥 RESPONSE STATUS: ${res.statusCode}`);
|
||||
console.log(`📥 RESPONSE HEADERS:`);
|
||||
console.log(res.headers);
|
||||
console.log('');
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('📄 RESPONSE BODY (XML):');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(data);
|
||||
console.log('─'.repeat(60));
|
||||
console.log('');
|
||||
|
||||
// Parse response
|
||||
if (data.includes('<Status>OK</Status>')) {
|
||||
console.log('✅ Status: OK');
|
||||
|
||||
// Extract OutputText
|
||||
const outputMatch = data.match(/<OutputText>([\s\S]*?)<\/OutputText>/);
|
||||
if (outputMatch) {
|
||||
const outputText = outputMatch[1];
|
||||
console.log('\n📝 SCRIPTALIZED TEXT (first 200 chars):');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(outputText.substring(0, 200) + '...');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`\n📏 OUTPUT LENGTH: ${outputText.length} characters`);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Error in response');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.error('❌ Request error:', err.message);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
console.error('❌ Request timeout');
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
198
Docker Backend/test-scriptalizer.js
Normal file
198
Docker Backend/test-scriptalizer.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Scriptalizer Separator Test
|
||||
* Tests which separator character survives Scriptalizer API processing
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const { parseStringPromise } = require('xml2js');
|
||||
|
||||
const LICENSE_KEY = 'f9918b40-d11c-11f0-b558-0800200c9a66';
|
||||
|
||||
// Test different separators
|
||||
const SEPARATORS = [
|
||||
{ name: 'Triple Pipe', value: '|||' },
|
||||
{ name: 'Triple Tilde', value: '~~~' },
|
||||
{ name: 'Triple Hash', value: '###' },
|
||||
{ name: 'Paragraph Signs', value: '§§§' },
|
||||
{ name: 'Double Pipe', value: '||' },
|
||||
{ name: 'Custom Marker', value: '___SKRIFT___' },
|
||||
{ name: 'Newline + Dashes', value: '\n---\n' },
|
||||
];
|
||||
|
||||
async function callScriptalizer(inputText) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Use x-www-form-urlencoded format
|
||||
const params = new URLSearchParams();
|
||||
params.append('LicenseKey', LICENSE_KEY);
|
||||
params.append('FontName', 'PremiumUltra79');
|
||||
params.append('ErrFrequency', '10');
|
||||
params.append('InputText', inputText);
|
||||
|
||||
const body = params.toString();
|
||||
|
||||
const options = {
|
||||
hostname: 'www.scriptalizer.co.uk',
|
||||
port: 443,
|
||||
path: '/QuantumScriptalize.asmx/Scriptalize',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', async () => {
|
||||
try {
|
||||
const result = await parseStringPromise(data, { explicitArray: false });
|
||||
const response = result.ScriptalizerResponse;
|
||||
|
||||
if (!response || response.Status !== 'OK') {
|
||||
reject(new Error(response?.Status || 'UNKNOWN_STATUS'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({ status: 'OK', outputText: response.OutputText });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function testSeparator(separator) {
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Testing: ${separator.name} (${JSON.stringify(separator.value)})`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// Create test input with 3 short texts separated by the separator
|
||||
const texts = [
|
||||
'Hallo Max Mustermann aus Berlin',
|
||||
'Liebe Anna Schmidt aus München',
|
||||
'Sehr geehrter Tom Weber aus Hamburg'
|
||||
];
|
||||
|
||||
const inputText = texts.join(separator.value);
|
||||
|
||||
console.log('\nInput:');
|
||||
console.log(inputText.substring(0, 150));
|
||||
console.log(`\nSeparator appears: ${(inputText.match(new RegExp(separator.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length} times`);
|
||||
|
||||
try {
|
||||
const result = await callScriptalizer(inputText);
|
||||
|
||||
console.log('\nOutput (first 200 chars):');
|
||||
console.log(result.outputText.substring(0, 200) + '...');
|
||||
|
||||
// Check if separator survived
|
||||
const separatorRegex = new RegExp(separator.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
||||
const separatorCount = (result.outputText.match(separatorRegex) || []).length;
|
||||
|
||||
console.log(`\nSeparator found in output: ${separatorCount} times`);
|
||||
|
||||
if (separatorCount === 2) {
|
||||
console.log('✅ SUCCESS: Separator survived!');
|
||||
|
||||
// Try to split
|
||||
const split = result.outputText.split(separator.value);
|
||||
console.log(`\nSplit result: ${split.length} parts`);
|
||||
|
||||
if (split.length === 3) {
|
||||
console.log('✅ PERFECT: Can split into 3 parts!');
|
||||
console.log('\nPart 1 (first 50 chars):', split[0].substring(0, 50) + '...');
|
||||
console.log('Part 2 (first 50 chars):', split[1].substring(0, 50) + '...');
|
||||
console.log('Part 3 (first 50 chars):', split[2].substring(0, 50) + '...');
|
||||
|
||||
return { separator: separator.name, value: separator.value, success: true, split: true };
|
||||
} else {
|
||||
console.log('⚠️ WARNING: Split count mismatch');
|
||||
return { separator: separator.name, value: separator.value, success: true, split: false };
|
||||
}
|
||||
} else {
|
||||
console.log('❌ FAILED: Separator was modified or not found');
|
||||
return { separator: separator.name, value: separator.value, success: false, found: separatorCount };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log(`\n❌ ERROR: ${err.message}`);
|
||||
return { separator: separator.name, value: separator.value, success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Scriptalizer Separator Test Suite ║');
|
||||
console.log('║ Testing which separator survives API processing ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝');
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const separator of SEPARATORS) {
|
||||
const result = await testSeparator(separator);
|
||||
results.push(result);
|
||||
|
||||
// Wait 2 seconds between tests to avoid rate limiting
|
||||
if (separator !== SEPARATORS[SEPARATORS.length - 1]) {
|
||||
console.log('\nWaiting 2 seconds before next test...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n\n' + '═'.repeat(60));
|
||||
console.log('SUMMARY');
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
const successful = results.filter(r => r.success && r.split);
|
||||
const partial = results.filter(r => r.success && !r.split);
|
||||
const failed = results.filter(r => !r.success);
|
||||
|
||||
console.log('\n✅ Fully Working Separators (can split):');
|
||||
if (successful.length === 0) {
|
||||
console.log(' None');
|
||||
} else {
|
||||
successful.forEach(r => console.log(` - ${r.separator} (${JSON.stringify(r.value)})`));
|
||||
}
|
||||
|
||||
console.log('\n⚠️ Partially Working Separators (found but cannot split):');
|
||||
if (partial.length === 0) {
|
||||
console.log(' None');
|
||||
} else {
|
||||
partial.forEach(r => console.log(` - ${r.separator}`));
|
||||
}
|
||||
|
||||
console.log('\n❌ Failed Separators:');
|
||||
if (failed.length === 0) {
|
||||
console.log(' None');
|
||||
} else {
|
||||
failed.forEach(r => console.log(` - ${r.separator} (${r.error || 'modified by API'})`));
|
||||
}
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
|
||||
if (successful.length > 0) {
|
||||
console.log(`\n🎉 RECOMMENDATION: Use "${successful[0].separator}" as separator`);
|
||||
console.log(` Separator value: ${JSON.stringify(successful[0].value)}`);
|
||||
} else if (partial.length > 0) {
|
||||
console.log(`\n⚠️ RECOMMENDATION: Use "${partial[0].separator}" but verify split logic`);
|
||||
} else {
|
||||
console.log('\n❌ No separator worked - need to use individual API calls');
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(console.error);
|
||||
12
Docker Backend/test-tilda-preview.json
Normal file
12
Docker Backend/test-tilda-preview.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"sessionId": "tilda-test-demo",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Sehr geehrte Damen und Herren,\n\nhiermit möchten wir Ihnen mitteilen, dass Ihre Bestellung erfolgreich bearbeitet wurde.\n\nWir bedanken uns für Ihr Vertrauen und freuen uns auf eine weitere Zusammenarbeit.\n\nMit freundlichen Grüßen\nIhr Skrift-Team",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
12
Docker Backend/test-variation.json
Normal file
12
Docker Backend/test-variation.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"sessionId": "variation-test",
|
||||
"letters": [
|
||||
{
|
||||
"index": 0,
|
||||
"format": "a4",
|
||||
"font": "tilda",
|
||||
"text": "Dies ist ein Test mit natürlicher Handschrift-Variation.\n\nDie Wörter haben unterschiedliche Abstände und eine leichte Schräglage.\n\nDas macht das Schriftbild authentischer und lebendiger.\n\nVielen Dank für Ihr Vertrauen!",
|
||||
"placeholders": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user