462 lines
7.9 KiB
Markdown
462 lines
7.9 KiB
Markdown
# 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
|