Align agent with updated backend API
This commit is contained in:
+126
-99
@@ -1,40 +1,89 @@
|
||||
# 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 einen 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.
|
||||
|
||||
### Query-Parameter
|
||||
|
||||
| Name | Pflicht | Beschreibung |
|
||||
| Parameter | Pflicht | Beschreibung |
|
||||
| --- | --- | --- |
|
||||
| `agentId` | ja | Eindeutige ID des Agents, z. B. Rechnername |
|
||||
| `agentId` | nein | Eindeutige Agent-ID, z. B. Rechnername; Fallback: `unknown` |
|
||||
|
||||
### 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 +92,7 @@ Content-Type: application/json
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "12345",
|
||||
"jobId": "42",
|
||||
"labelImageBase64": "iVBORw0KGgoAAAANSUhEUgAA...",
|
||||
"labelImageContentType": "image/png",
|
||||
"labelWidthMm": 57,
|
||||
@@ -51,34 +100,18 @@ 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
|
||||
## 4. Etikettbild separat abrufen
|
||||
|
||||
| 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` |
|
||||
|
||||
`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 +124,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
|
||||
```
|
||||
|
||||
## 3. Erfolgreichen Druck melden
|
||||
Job oder Bild nicht vorhanden.
|
||||
|
||||
## 5. Erfolgreichen Druck melden
|
||||
|
||||
```http
|
||||
POST /api/label-print-agent/jobs/{jobId}/printed
|
||||
@@ -111,21 +148,21 @@ 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 +175,7 @@ Content-Type: application/json
|
||||
{
|
||||
"agentId": "PC-BUERO",
|
||||
"printerName": "DYMO LabelWriter 450",
|
||||
"errorMessage": "Drucker ist nicht verfügbar."
|
||||
"errorMessage": "Drucker nicht verfügbar."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -148,63 +185,53 @@ Content-Type: application/json
|
||||
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
|
||||
## Job-Lebenszyklus
|
||||
|
||||
Empfohlener Ablauf im Backend:
|
||||
```text
|
||||
createJob()
|
||||
|
|
||||
v
|
||||
pending ---- jobs/next ---- Lock (5 Min TTL)
|
||||
|
|
||||
Agent druckt
|
||||
|
|
||||
+-------+-------+
|
||||
printed error
|
||||
(PrintedUrl) (ReleaseUrl)
|
||||
```
|
||||
|
||||
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.
|
||||
## 7. Server-Sent Events – neue Druckaufträge (Push)
|
||||
|
||||
## Empfohlene Statuscodes
|
||||
```http
|
||||
GET /api/label-print-agent/events
|
||||
Authorization: Bearer {token}
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
Der Agent verbindet sich einmalig. Sobald ein neuer Druckauftrag erstellt wird, sendet das Backend:
|
||||
|
||||
```text
|
||||
data: {"type":"label-job-available"}
|
||||
```
|
||||
|
||||
Der Agent ruft daraufhin sofort `GET /jobs/next` auf. Polling bleibt als Fallback sinnvoll, z. B. alle 30 Sekunden, falls die SSE-Verbindung unterbrochen wurde.
|
||||
|
||||
Es werden keine `agentId`-Parameter ausgewertet; alle verbundenen Agents erhalten das Event.
|
||||
|
||||
## 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 / 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` |
|
||||
|
||||
## Server-Sent Events optional
|
||||
|
||||
Später kann das Backend zusätzlich einen Event-Endpunkt anbieten:
|
||||
|
||||
```http
|
||||
GET /api/label-print-agent/events?agentId={agentId}
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
event: label-job-available
|
||||
data: {"count":1}
|
||||
```
|
||||
|
||||
Der Agent könnte dann bei einem Event sofort `jobs/next` aufrufen. Polling bleibt trotzdem als Fallback sinnvoll.
|
||||
|
||||
## Wichtige Designentscheidung
|
||||
|
||||
Der Agent kennt keine fachlichen Layouts mehr:
|
||||
|
||||
- keine `layout_key`
|
||||
- keine lokalen LabelTemplates
|
||||
- keine MySQL-Verbindung
|
||||
- keine Nummernreservierung
|
||||
- kein QR-Code-Rendering
|
||||
|
||||
Das Backend liefert ein fertiges Bild. Der Agent ist nur noch lokaler Windows-Druck-Connector.
|
||||
|
||||
@@ -15,7 +15,7 @@ LabelPrintAgent
|
||||
-> meldet Erfolg oder Fehler ans Backend zurück
|
||||
```
|
||||
|
||||
Optional kann später statt Polling Server-Sent Events ergänzt werden. Der aktuelle Stand nutzt bewusst Polling, weil es robuster und einfacher zu betreiben ist.
|
||||
Der Agent unterstützt Server-Sent Events für Push-Benachrichtigungen und nutzt Polling als Fallback.
|
||||
|
||||
## Backend-Vertrag
|
||||
|
||||
@@ -102,8 +102,11 @@ Beispiel:
|
||||
"agentId": "PC-BUERO",
|
||||
"encryptedApiToken": "",
|
||||
"nextJobPath": "/api/label-print-agent/jobs/next",
|
||||
"imagePath": "/api/label-print-agent/jobs/{jobId}/image",
|
||||
"reportSuccessPath": "/api/label-print-agent/jobs/{jobId}/printed",
|
||||
"reportErrorPath": "/api/label-print-agent/jobs/{jobId}/error"
|
||||
"reportErrorPath": "/api/label-print-agent/jobs/{jobId}/error",
|
||||
"useServerSentEvents": true,
|
||||
"eventsPath": "/api/label-print-agent/events"
|
||||
},
|
||||
"printer": {
|
||||
"printerName": "DYMO LabelWriter 450",
|
||||
@@ -113,7 +116,7 @@ Beispiel:
|
||||
},
|
||||
"worker": {
|
||||
"enabled": true,
|
||||
"pollIntervalSeconds": 5
|
||||
"pollIntervalSeconds": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,8 +24,11 @@ internal sealed class SettingsForm : Form
|
||||
private readonly TextBox _agentId = new() { Width = 260 };
|
||||
private readonly TextBox _apiToken = new() { Width = 420, UseSystemPasswordChar = true };
|
||||
private readonly TextBox _nextJobPath = new() { Width = 420 };
|
||||
private readonly TextBox _imagePath = new() { Width = 420 };
|
||||
private readonly TextBox _reportSuccessPath = new() { Width = 420 };
|
||||
private readonly TextBox _reportErrorPath = new() { Width = 420 };
|
||||
private readonly CheckBox _useServerSentEvents = new() { Text = "Server-Sent Events verwenden", AutoSize = true };
|
||||
private readonly TextBox _eventsPath = new() { Width = 420 };
|
||||
|
||||
private readonly ComboBox _printer = new() { Width = 360, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
private readonly NumericUpDown _labelWidth = new() { Minimum = 1, Maximum = 500, DecimalPlaces = 1, Value = 57, Width = 100 };
|
||||
@@ -82,10 +85,13 @@ internal sealed class SettingsForm : Form
|
||||
panel.Controls.Add(Row(60, Label("AgentId", 160), _agentId));
|
||||
panel.Controls.Add(Row(100, Label("API Token", 160), _apiToken));
|
||||
panel.Controls.Add(Row(140, Label("NextJobPath", 160), _nextJobPath));
|
||||
panel.Controls.Add(Row(180, Label("ReportSuccessPath", 160), _reportSuccessPath));
|
||||
panel.Controls.Add(Row(220, Label("ReportErrorPath", 160), _reportErrorPath));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 180, 275, (_, _) => SaveBackend(showMessage: true)));
|
||||
panel.Controls.Add(ButtonAt("Jetzt prüfen", 300, 275, async (_, _) => await PollOnceFromUiAsync()));
|
||||
panel.Controls.Add(Row(180, Label("ImagePath", 160), _imagePath));
|
||||
panel.Controls.Add(Row(220, Label("ReportSuccessPath", 160), _reportSuccessPath));
|
||||
panel.Controls.Add(Row(260, Label("ReportErrorPath", 160), _reportErrorPath));
|
||||
panel.Controls.Add(Row(300, Label("SSE", 160), _useServerSentEvents));
|
||||
panel.Controls.Add(Row(340, Label("EventsPath", 160), _eventsPath));
|
||||
panel.Controls.Add(ButtonAt("Speichern", 180, 395, (_, _) => SaveBackend(showMessage: true)));
|
||||
panel.Controls.Add(ButtonAt("Jetzt prüfen", 300, 395, async (_, _) => await PollOnceFromUiAsync()));
|
||||
return new TabPage("Backend") { Controls = { panel } };
|
||||
}
|
||||
|
||||
@@ -119,8 +125,11 @@ internal sealed class SettingsForm : Form
|
||||
_agentId.Text = settings.Backend.AgentId;
|
||||
_apiToken.Text = _settingsStore.DecryptBackendApiToken(settings);
|
||||
_nextJobPath.Text = settings.Backend.NextJobPath;
|
||||
_imagePath.Text = settings.Backend.ImagePath;
|
||||
_reportSuccessPath.Text = settings.Backend.ReportSuccessPath;
|
||||
_reportErrorPath.Text = settings.Backend.ReportErrorPath;
|
||||
_useServerSentEvents.Checked = settings.Backend.UseServerSentEvents;
|
||||
_eventsPath.Text = settings.Backend.EventsPath;
|
||||
|
||||
_labelWidth.Value = settings.Printer.LabelWidthMm;
|
||||
_labelHeight.Value = settings.Printer.LabelHeightMm;
|
||||
@@ -161,8 +170,11 @@ internal sealed class SettingsForm : Form
|
||||
settings.Backend.AgentId = _agentId.Text.Trim();
|
||||
settings.Backend.EncryptedApiToken = _settingsStore.EncryptPassword(_apiToken.Text);
|
||||
settings.Backend.NextJobPath = _nextJobPath.Text.Trim();
|
||||
settings.Backend.ImagePath = _imagePath.Text.Trim();
|
||||
settings.Backend.ReportSuccessPath = _reportSuccessPath.Text.Trim();
|
||||
settings.Backend.ReportErrorPath = _reportErrorPath.Text.Trim();
|
||||
settings.Backend.UseServerSentEvents = _useServerSentEvents.Checked;
|
||||
settings.Backend.EventsPath = _eventsPath.Text.Trim();
|
||||
_settingsStore.Save(settings);
|
||||
AppendStatus("Backend-Einstellungen gespeichert.");
|
||||
if (showMessage)
|
||||
|
||||
@@ -108,11 +108,6 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
return (false, "Backend-URL fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.Backend.AgentId))
|
||||
{
|
||||
return (false, "Agent-ID fehlt.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.Printer.PrinterName))
|
||||
{
|
||||
return (false, "Drucker fehlt.");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Drawing;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using LabelPrintAgent.Configuration;
|
||||
|
||||
namespace LabelPrintAgent.Backend;
|
||||
@@ -41,12 +42,15 @@ public sealed class BackendClient
|
||||
|
||||
if (string.IsNullOrWhiteSpace(job.LabelImageUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Backend-Job enthält weder labelImageBase64 noch labelImageUrl.");
|
||||
var settingsForPath = _settingsStore.Load();
|
||||
job.LabelImageUrl = settingsForPath.Backend.ImagePath.Replace("{jobId}", Uri.EscapeDataString(job.JobId), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var settings = _settingsStore.Load();
|
||||
using var request = CreateRequest(HttpMethod.Get, MakeAbsoluteUrl(settings.Backend.BaseUrl, job.LabelImageUrl), settings);
|
||||
var bytesFromUrl = await (await _httpClient.SendAsync(request, cancellationToken)).Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var bytesFromUrl = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
return CreateBitmap(bytesFromUrl);
|
||||
}
|
||||
|
||||
@@ -82,6 +86,33 @@ public sealed class BackendClient
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task WatchServerSentEventsAsync(Func<CancellationToken, Task> onLabelJobAvailable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
using var request = CreateRequest(HttpMethod.Get, MakeAbsoluteUrl(settings.Backend.BaseUrl, settings.Backend.EventsPath), settings);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(cancellationToken);
|
||||
if (line is null || !line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = line["data:".Length..].Trim();
|
||||
if (IsLabelJobAvailableEvent(data))
|
||||
{
|
||||
await onLabelJobAvailable(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string url, AppSettings settings)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
@@ -97,7 +128,8 @@ public sealed class BackendClient
|
||||
private static string BuildUrl(string baseUrl, string path, string agentId)
|
||||
{
|
||||
var separator = path.Contains('?') ? '&' : '?';
|
||||
return $"{MakeAbsoluteUrl(baseUrl, path)}{separator}agentId={Uri.EscapeDataString(agentId)}";
|
||||
var effectiveAgentId = string.IsNullOrWhiteSpace(agentId) ? "unknown" : agentId;
|
||||
return $"{MakeAbsoluteUrl(baseUrl, path)}{separator}agentId={Uri.EscapeDataString(effectiveAgentId)}";
|
||||
}
|
||||
|
||||
private static string MakeAbsoluteUrl(string baseUrl, string pathOrUrl)
|
||||
@@ -117,4 +149,23 @@ public sealed class BackendClient
|
||||
using var stream = new MemoryStream(bytes);
|
||||
return new Bitmap(stream);
|
||||
}
|
||||
|
||||
private static bool IsLabelJobAvailableEvent(string data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(data);
|
||||
return document.RootElement.TryGetProperty("type", out var type)
|
||||
&& string.Equals(type.GetString(), "label-job-available", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return data.Contains("label-job-available", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _task;
|
||||
private Task? _sseTask;
|
||||
private AgentHealthStatus _lastStatus = new(false, "Noch nicht mit dem Backend verbunden.");
|
||||
|
||||
public BackendPollingWorker(SettingsStore settingsStore, BackendClient backendClient, PrinterService printerService)
|
||||
@@ -34,6 +35,7 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_task = Task.Run(() => RunAsync(_cancellationTokenSource.Token));
|
||||
_sseTask = Task.Run(() => RunServerSentEventsAsync(_cancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
public async Task PollOnceAsync(CancellationToken cancellationToken = default)
|
||||
@@ -122,12 +124,46 @@ public sealed class BackendPollingWorker : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunServerSentEventsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var settings = _settingsStore.Load();
|
||||
if (!settings.Worker.Enabled || !settings.Backend.UseServerSentEvents || string.IsNullOrWhiteSpace(settings.Backend.BaseUrl))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SetStatus(LastStatus.IsHealthy, "SSE-Verbindung wird aufgebaut.");
|
||||
await _backendClient.WatchServerSentEventsAsync(async token =>
|
||||
{
|
||||
SetStatus(true, "SSE: neuer Druckjob gemeldet.");
|
||||
await PollOnceAsync(token);
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Server-sent events connection failed");
|
||||
SetStatus(false, $"SSE getrennt: {ex.Message}");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
try
|
||||
{
|
||||
_task?.Wait(TimeSpan.FromSeconds(2));
|
||||
_sseTask?.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -20,8 +20,11 @@ public sealed class BackendSettings
|
||||
public string AgentId { get; set; } = Environment.MachineName;
|
||||
public string EncryptedApiToken { get; set; } = string.Empty;
|
||||
public string NextJobPath { get; set; } = "/api/label-print-agent/jobs/next";
|
||||
public string ImagePath { get; set; } = "/api/label-print-agent/jobs/{jobId}/image";
|
||||
public string ReportSuccessPath { get; set; } = "/api/label-print-agent/jobs/{jobId}/printed";
|
||||
public string ReportErrorPath { get; set; } = "/api/label-print-agent/jobs/{jobId}/error";
|
||||
public bool UseServerSentEvents { get; set; } = true;
|
||||
public string EventsPath { get; set; } = "/api/label-print-agent/events";
|
||||
}
|
||||
|
||||
public sealed class PrinterSettings
|
||||
@@ -35,7 +38,7 @@ public sealed class PrinterSettings
|
||||
public sealed class WorkerSettings
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int PollIntervalSeconds { get; set; } = 5;
|
||||
public int PollIntervalSeconds { get; set; } = 30;
|
||||
}
|
||||
|
||||
public sealed class PathSettings
|
||||
|
||||
Reference in New Issue
Block a user