From 8d0bef38c0593f6dff924d2045d9dae595ef6ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Thu, 7 May 2026 22:21:06 +0200 Subject: [PATCH] Add tray health printer icons --- README.md | 7 ++ .../App/TrayApplicationContext.cs | 73 ++++++++++++++++++- src/LabelPrintAgent/App/TrayIconFactory.cs | 52 +++++++++++++ .../Backend/AgentHealthStatus.cs | 15 ++++ .../Backend/BackendPollingWorker.cs | 24 ++++++ 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/LabelPrintAgent/App/TrayIconFactory.cs create mode 100644 src/LabelPrintAgent/Backend/AgentHealthStatus.cs diff --git a/README.md b/README.md index 5415d88..b3fee13 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/LabelPrintAgent/App/TrayApplicationContext.cs b/src/LabelPrintAgent/App/TrayApplicationContext.cs index ccca567..e7bec17 100644 --- a/src/LabelPrintAgent/App/TrayApplicationContext.cs +++ b/src/LabelPrintAgent/App/TrayApplicationContext.cs @@ -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]; + } } diff --git a/src/LabelPrintAgent/App/TrayIconFactory.cs b/src/LabelPrintAgent/App/TrayIconFactory.cs new file mode 100644 index 0000000..a252614 --- /dev/null +++ b/src/LabelPrintAgent/App/TrayIconFactory.cs @@ -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); +} diff --git a/src/LabelPrintAgent/Backend/AgentHealthStatus.cs b/src/LabelPrintAgent/Backend/AgentHealthStatus.cs new file mode 100644 index 0000000..58e0ae3 --- /dev/null +++ b/src/LabelPrintAgent/Backend/AgentHealthStatus.cs @@ -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; } +} diff --git a/src/LabelPrintAgent/Backend/BackendPollingWorker.cs b/src/LabelPrintAgent/Backend/BackendPollingWorker.cs index 7128169..137f23e 100644 --- a/src/LabelPrintAgent/Backend/BackendPollingWorker.cs +++ b/src/LabelPrintAgent/Backend/BackendPollingWorker.cs @@ -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? 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); + } }