Add tray health printer icons

This commit is contained in:
2026-05-07 22:21:06 +02:00
parent feb0b384ae
commit 8d0bef38c0
5 changed files with 169 additions and 2 deletions
+7
View File
@@ -140,6 +140,13 @@ Wichtig:
5. Im Tab `Allgemein` Polling aktivieren und Intervall setzen.
6. Mit `Jetzt prüfen` kann sofort ein einzelner Backend-Poll ausgelöst werden.
## Tray-Status
Das Tray-Icon zeigt den aktuellen Zustand:
- Grün: Worker ist aktiviert, Backend-Konfiguration ist vollständig, Drucker ist verfügbar und der letzte Backend-Kontakt war erfolgreich.
- Rot: Konfiguration fehlt, Drucker ist nicht verfügbar, Worker ist deaktiviert oder der Backend-Kontakt ist fehlgeschlagen.
## Nicht mehr im Agent
- keine Layout-JSON-Verwaltung
@@ -11,6 +11,9 @@ internal sealed class TrayApplicationContext : ApplicationContext
private readonly SettingsStore _settingsStore;
private readonly PrinterService _printerService;
private readonly BackendPollingWorker _backendWorker;
private readonly Icon _healthyIcon;
private readonly Icon _unhealthyIcon;
private readonly System.Windows.Forms.Timer _statusTimer;
private SettingsForm? _settingsForm;
public TrayApplicationContext(
@@ -21,6 +24,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
_settingsStore = settingsStore;
_printerService = printerService;
_backendWorker = backendWorker;
_healthyIcon = TrayIconFactory.CreatePrinterIcon(Color.FromArgb(36, 160, 72));
_unhealthyIcon = TrayIconFactory.CreatePrinterIcon(Color.FromArgb(210, 48, 48));
var menu = new ContextMenuStrip();
menu.Items.Add("Einstellungen", null, (_, _) => ShowSettings());
@@ -28,8 +33,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
_notifyIcon = new NotifyIcon
{
Text = "LabelPrintAgent",
Icon = SystemIcons.Application,
Text = "LabelPrintAgent: nicht verbunden",
Icon = _unhealthyIcon,
ContextMenuStrip = menu,
Visible = true
};
@@ -40,6 +45,11 @@ internal sealed class TrayApplicationContext : ApplicationContext
ShowSettings();
}
};
_statusTimer = new System.Windows.Forms.Timer { Interval = 5000 };
_statusTimer.Tick += (_, _) => UpdateTrayStatus();
_statusTimer.Start();
UpdateTrayStatus();
}
private void ShowSettings()
@@ -64,9 +74,68 @@ internal sealed class TrayApplicationContext : ApplicationContext
protected override void ExitThreadCore()
{
_statusTimer.Stop();
_statusTimer.Dispose();
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
_healthyIcon.Dispose();
_unhealthyIcon.Dispose();
_settingsForm?.Dispose();
base.ExitThreadCore();
}
private void UpdateTrayStatus()
{
var (isConfigured, configurationMessage) = GetConfigurationStatus();
var workerStatus = _backendWorker.LastStatus;
var isHealthy = isConfigured && workerStatus.IsHealthy;
_notifyIcon.Icon = isHealthy ? _healthyIcon : _unhealthyIcon;
_notifyIcon.Text = BuildTooltip(isHealthy, isConfigured ? workerStatus.Message : configurationMessage);
}
private (bool IsConfigured, string Message) GetConfigurationStatus()
{
try
{
var settings = _settingsStore.Load();
if (!settings.Worker.Enabled)
{
return (false, "Worker deaktiviert.");
}
if (string.IsNullOrWhiteSpace(settings.Backend.BaseUrl))
{
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.");
}
if (!_printerService.IsPrinterAvailable(settings.Printer.PrinterName))
{
return (false, "Drucker nicht verfügbar.");
}
return (true, "Konfiguration vollständig.");
}
catch (Exception ex)
{
Log.Warning(ex, "Could not evaluate tray status");
return (false, "Statusprüfung fehlgeschlagen.");
}
}
private static string BuildTooltip(bool isHealthy, string message)
{
var prefix = isHealthy ? "LabelPrintAgent: verbunden" : "LabelPrintAgent: nicht bereit";
var tooltip = $"{prefix} - {message}";
return tooltip.Length <= 63 ? tooltip : tooltip[..63];
}
}
@@ -0,0 +1,52 @@
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
namespace LabelPrintAgent.App;
internal static class TrayIconFactory
{
public static Icon CreatePrinterIcon(Color statusColor)
{
using var bitmap = new Bitmap(32, 32);
using var graphics = Graphics.FromImage(bitmap);
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.Clear(Color.Transparent);
using var statusBrush = new SolidBrush(statusColor);
using var blackBrush = new SolidBrush(Color.FromArgb(32, 32, 32));
using var whiteBrush = new SolidBrush(Color.White);
using var outlinePen = new Pen(Color.FromArgb(32, 32, 32), 2);
using var paperPen = new Pen(Color.FromArgb(32, 32, 32), 1.5f);
graphics.FillEllipse(statusBrush, 1, 1, 30, 30);
graphics.DrawEllipse(outlinePen, 1.5f, 1.5f, 29, 29);
var paper = new RectangleF(9, 5, 14, 9);
graphics.FillRectangle(whiteBrush, paper);
graphics.DrawRectangle(paperPen, paper.X, paper.Y, paper.Width, paper.Height);
var printerBody = new RectangleF(6, 13, 20, 10);
graphics.FillRectangle(blackBrush, printerBody);
graphics.DrawRectangle(outlinePen, printerBody.X, printerBody.Y, printerBody.Width, printerBody.Height);
var outputPaper = new RectangleF(10, 20, 12, 7);
graphics.FillRectangle(whiteBrush, outputPaper);
graphics.DrawRectangle(paperPen, outputPaper.X, outputPaper.Y, outputPaper.Width, outputPaper.Height);
graphics.FillEllipse(whiteBrush, 21, 15, 2.5f, 2.5f);
var handle = bitmap.GetHicon();
try
{
using var icon = Icon.FromHandle(handle);
return (Icon)icon.Clone();
}
finally
{
DestroyIcon(handle);
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
}
@@ -0,0 +1,15 @@
namespace LabelPrintAgent.Backend;
public sealed class AgentHealthStatus
{
public AgentHealthStatus(bool isHealthy, string message)
{
IsHealthy = isHealthy;
Message = message;
ChangedAt = DateTime.Now;
}
public bool IsHealthy { get; }
public string Message { get; }
public DateTime ChangedAt { get; }
}
@@ -12,6 +12,7 @@ public sealed class BackendPollingWorker : IDisposable
private readonly SemaphoreSlim _semaphore = new(1, 1);
private CancellationTokenSource? _cancellationTokenSource;
private Task? _task;
private AgentHealthStatus _lastStatus = new(false, "Noch nicht mit dem Backend verbunden.");
public BackendPollingWorker(SettingsStore settingsStore, BackendClient backendClient, PrinterService printerService)
{
@@ -20,6 +21,10 @@ public sealed class BackendPollingWorker : IDisposable
_printerService = printerService;
}
public event EventHandler<AgentHealthStatus>? StatusChanged;
public AgentHealthStatus LastStatus => _lastStatus;
public void Start()
{
if (_task is not null)
@@ -43,6 +48,7 @@ public sealed class BackendPollingWorker : IDisposable
var settings = _settingsStore.Load();
if (!settings.Worker.Enabled || string.IsNullOrWhiteSpace(settings.Backend.BaseUrl))
{
SetStatus(false, "Worker deaktiviert oder Backend-URL fehlt.");
return;
}
@@ -50,12 +56,14 @@ public sealed class BackendPollingWorker : IDisposable
if (string.IsNullOrWhiteSpace(printerName) || !_printerService.IsPrinterAvailable(printerName))
{
Log.Warning("No available printer configured for backend polling");
SetStatus(false, "Kein verfügbarer Drucker konfiguriert.");
return;
}
var job = await _backendClient.GetNextJobAsync(cancellationToken);
if (job is null)
{
SetStatus(true, "Backend verbunden. Kein Druckjob vorhanden.");
return;
}
@@ -66,17 +74,25 @@ public sealed class BackendPollingWorker : IDisposable
if (result.Success)
{
await _backendClient.ReportPrintedAsync(job.JobId, printerName, cancellationToken);
SetStatus(true, $"Job {job.JobId} erfolgreich gedruckt.");
return;
}
await _backendClient.ReportErrorAsync(job.JobId, printerName, result.ErrorMessage ?? "Druck fehlgeschlagen.", cancellationToken);
SetStatus(false, $"Job {job.JobId} konnte nicht gedruckt werden.");
}
catch (Exception ex)
{
Log.Error(ex, "Could not process backend label job {JobId}", job.JobId);
await _backendClient.ReportErrorAsync(job.JobId, printerName, ex.Message, cancellationToken);
SetStatus(false, $"Job {job.JobId} fehlgeschlagen: {ex.Message}");
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
SetStatus(false, $"Backend nicht verbunden: {ex.Message}");
throw;
}
finally
{
_semaphore.Release();
@@ -98,6 +114,7 @@ public sealed class BackendPollingWorker : IDisposable
catch (Exception ex)
{
Log.Error(ex, "Backend polling failed");
SetStatus(false, $"Backend nicht verbunden: {ex.Message}");
}
var interval = Math.Max(1, _settingsStore.Load().Worker.PollIntervalSeconds);
@@ -119,4 +136,11 @@ public sealed class BackendPollingWorker : IDisposable
_cancellationTokenSource?.Dispose();
_semaphore.Dispose();
}
private void SetStatus(bool isHealthy, string message)
{
var status = new AgentHealthStatus(isHealthy, message);
_lastStatus = status;
StatusChanged?.Invoke(this, status);
}
}