feat: add SSE event stream for print jobs, implement batch printing in frontend, and update API documentation.
Build and Push Multi-Platform Images / build-and-push (push) Successful in 36s

This commit is contained in:
bjoernpoettker committed 2026-05-09 09:04:20 +02:00
1 parent 3683fe9487
commit f4428afb9b
4 files changed
+181 -100

No files matched your search

+133 -95
View File
@@ -1,40 +1,94 @@
# Backend API für LabelPrintAgent
# Backend API – LabelPrintAgent
Diese Datei beschreibt die Endpunkte, die der PaperlessManager bereitstellen muss, damit der LabelPrintAgent Etiketten abholen, drucken und das Ergebnis zurückmelden kann.
Diese Datei beschreibt die Endpunkte des PaperlessManager-Backends für den LabelPrintAgent.
Der LabelPrintAgent rendert keine Layouts selbst. Das Backend liefert ein fertiges Etikettbild.
Das Backend rendert das fertige Etikettbild (SVG → PNG via resvg-js). Der Agent ist nur lokaler Druck-Connector.
## Authentifizierung
Alle Endpunkte sollten denselben Bearer Token akzeptieren:
Alle Endpunkte erfordern einen Bearer Token (JWT oder API-Key):
```http
Authorization: Bearer {apiToken}
Authorization: Bearer {token}
```
Der Token wird im Agent lokal verschlüsselt gespeichert.
`/jobs/next`, `/jobs/:id/image`, `/jobs/:id/printed` und `/jobs/:id/error` benötigen **keine** spezifische Permission (nur gültigen Token). `POST /jobs` und `POST /preview` erfordern `VIEW_SCANNER`.
## 1. Nächsten Druckjob abrufen
---
## 1. Job manuell anlegen (Frontend → Backend)
```http
POST /api/label-print-agent/jobs
Content-Type: application/json
```
### Request Body
```json
{
"templateId": 3,
"fieldValues": {
"datum": "2026-05-09"
}
}
```
| Feld | Pflicht | Beschreibung |
|---|---|---|
| `templateId` | ja | ID des BarcodeTemplates |
| `fieldValues` | nein | Feldwerte für Platzhalter; Datumsfelder im Format `YYYY-MM-DD` |
### Antwort
```http
201 Created
```
```json
{ "jobId": "42" }
```
---
## 2. Vorschau-Bild rendern (kein Job, keine Nummer-Reservierung)
```http
POST /api/label-print-agent/preview
Content-Type: application/json
```
### Request Body
Identisch mit `POST /jobs`. `{number}` wird immer als `1` gerendert — die GET-URL wird **nicht** aufgerufen.
### Antwort
```http
200 OK
Content-Type: image/png
```
Body: binäres PNG-Bild.
---
## 3. Nächsten Druckjob abrufen (Agent-Polling)
```http
GET /api/label-print-agent/jobs/next?agentId={agentId}
```
Der Agent ruft diesen Endpunkt alle X Sekunden auf.
| Parameter | Pflicht | Beschreibung |
|---|---|---|
| `agentId` | nein | Eindeutige Agent-ID (z. B. Rechnername); Fallback: `"unknown"` |
### Query-Parameter
| Name | Pflicht | Beschreibung |
| --- | --- | --- |
| `agentId` | ja | Eindeutige ID des Agents, z. B. Rechnername |
### Antwort, wenn kein Job vorhanden ist
### Antwort – kein Job vorhanden
```http
204 No Content
```
### Antwort mit Bild direkt im JSON
### Antwort – Job vorhanden
```http
200 OK
@@ -43,7 +97,7 @@ Content-Type: application/json
```json
{
"jobId": "12345",
"jobId": "42",
"labelImageBase64": "iVBORw0KGgoAAAANSUhEUgAA...",
"labelImageContentType": "image/png",
"labelWidthMm": 57,
@@ -51,34 +105,20 @@ Content-Type: application/json
}
```
### Antwort mit separater Bild-URL
| Feld | Beschreibung |
|---|---|
| `jobId` | Job-ID für Rückmeldungen |
| `labelImageBase64` | Base64-PNG; `null` wenn Rendering fehlgeschlagen |
| `labelImageContentType` | Immer `"image/png"` |
| `labelWidthMm` / `labelHeightMm` | Etikettenmaß in mm |
```json
{
"jobId": "12345",
"labelImageUrl": "/api/label-print-agent/jobs/12345/image",
"labelImageContentType": "image/png",
"labelWidthMm": 57,
"labelHeightMm": 32
}
```
Das Backend setzt beim Ausliefern einen Lock (5-Minuten-TTL). Jobs mit abgelaufenem Lock werden erneut angeboten.
### Felder
---
| Feld | Pflicht | Beschreibung |
| --- | --- | --- |
| `jobId` | ja | Eindeutige Job-ID für Rückmeldungen |
| `labelImageBase64` | bedingt | Base64-kodiertes Etikettbild |
| `labelImageUrl` | bedingt | URL zum Nachladen des Etikettbilds |
| `labelImageContentType` | empfohlen | z. B. `image/png` |
| `labelWidthMm` | ja | Etikettenbreite in mm, z. B. `57` |
| `labelHeightMm` | ja | Etikettenhöhe in mm, z. B. `32` |
## 4. Etikettbild separat abrufen
`labelImageBase64` oder `labelImageUrl` muss gesetzt sein.
## 2. Etikettbild nachladen
Nur erforderlich, wenn `labelImageUrl` verwendet wird.
Alternativ zum Base64-Feld in `jobs/next`.
```http
GET /api/label-print-agent/jobs/{jobId}/image
@@ -91,11 +131,15 @@ GET /api/label-print-agent/jobs/{jobId}/image
Content-Type: image/png
```
Body: Binärdaten des fertigen Etikettbilds.
Body: binäres PNG-Bild.
Empfehlung: PNG, schwarz/weiß, passend zum Etikettenformat, z. B. 57 x 32 mm bei 300 dpi.
```http
404 Not Found – Job oder Bild nicht vorhanden
```
## 3. Erfolgreichen Druck melden
---
## 5. Erfolgreichen Druck melden
```http
POST /api/label-print-agent/jobs/{jobId}/printed
@@ -111,21 +155,22 @@ Content-Type: application/json
}
```
Alle Felder optional; Fallback jeweils `""` / `"unknown"`.
### Antwort
```http
200 OK
```
oder:
```http
204 No Content
```json
{ "ok": true }
```
Das Backend sollte den Job erst hier endgültig als gedruckt markieren.
Das Backend setzt den Job auf `printed`, speichert Zeitstempel und ruft die konfigurierte `LabelPrintedUrl` des Templates auf (`POST`).
## 4. Fehler melden
---
## 6. Druckfehler melden
```http
POST /api/label-print-agent/jobs/{jobId}/error
@@ -138,7 +183,7 @@ Content-Type: application/json
{
"agentId": "PC-BUERO",
"printerName": "DYMO LabelWriter 450",
"errorMessage": "Drucker ist nicht verfügbar."
"errorMessage": "Drucker nicht verfügbar."
}
```
@@ -147,64 +192,57 @@ Content-Type: application/json
```http
200 OK
```
oder:
```http
204 No Content
```json
{ "ok": true }
```
Das Backend entscheidet danach, ob der Job erneut angeboten wird oder auf Fehler bleibt.
Das Backend setzt den Job auf `error` und ruft die konfigurierte `LabelReleaseUrl` des Templates auf (`POST`).
## Backend-Verhalten
---
Empfohlener Ablauf im Backend:
## Job-Lebenszyklus
1. Job erstellen und serverseitig Layout, Nummern, QR-Code und Bild erzeugen.
2. Job bleibt wartend, bis ein Agent ihn abholt.
3. `jobs/next` liefert jeweils höchstens einen Job.
4. Backend reserviert oder lockt den Job beim Ausliefern, damit zwei Agents ihn nicht parallel drucken.
5. Agent druckt lokal.
6. Agent meldet `printed` oder `error`.
7. Backend setzt den finalen Status.
```
createJob()
│
▼
pending ──── jobs/next ──── Lock (5 Min TTL)
│
Agent druckt
│
┌───────┴───────┐
printed error
(PrintedUrl) (ReleaseUrl)
```
## Empfohlene Statuscodes
| Situation | Status |
| --- | --- |
| Kein Job vorhanden | `204 No Content` |
| Job vorhanden | `200 OK` |
| Token fehlt/ungültig | `401 Unauthorized` |
| Agent darf nicht drucken | `403 Forbidden` |
| Job-ID unbekannt | `404 Not Found` |
| Backend-Fehler | `500 Internal Server Error` |
## Server-Sent Events optional
Später kann das Backend zusätzlich einen Event-Endpunkt anbieten:
## 7. Server-Sent Events – neue Druckaufträge (Push)
```http
GET /api/label-print-agent/events?agentId={agentId}
GET /api/label-print-agent/events
Authorization: Bearer {token}
Accept: text/event-stream
```
Beispiel:
Der Agent verbindet sich einmalig. Sobald ein neuer Druckauftrag erstellt wird, sendet das Backend:
```text
event: label-job-available
data: {"count":1}
```
data: {"type":"label-job-available"}
```
Der Agent könnte dann bei einem Event sofort `jobs/next` aufrufen. Polling bleibt trotzdem als Fallback sinnvoll.
Der Agent ruft daraufhin sofort `GET /jobs/next` auf. Polling bleibt als Fallback sinnvoll (z. B. alle 30 s), falls die SSE-Verbindung unterbrochen wurde.
## Wichtige Designentscheidung
Es werden keine agentId-Parameter ausgewertet – alle verbundenen Agents erhalten das Event.
Der Agent kennt keine fachlichen Layouts mehr:
---
- keine `layout_key`
- keine lokalen LabelTemplates
- keine MySQL-Verbindung
- keine Nummernreservierung
- kein QR-Code-Rendering
## Statuscodes
Das Backend liefert ein fertiges Bild. Der Agent ist nur noch lokaler Windows-Druck-Connector.
| Situation | Status |
|---|---|
| Kein Job vorhanden | `204 No Content` |
| Job / Bild vorhanden | `200 OK` |
| Job erstellt | `201 Created` |
| Token fehlt / ungültig | `401 Unauthorized` |
| Fehlende Permission | `403 Forbidden` |
| Job-ID unbekannt | `404 Not Found` |
| Backend-Fehler | `500 Internal Server Error` |