diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs
index 1f44a89..ba453e5 100644
--- a/agent-cs/AgentWorker.cs
+++ b/agent-cs/AgentWorker.cs
@@ -6,7 +6,7 @@ namespace ITNexusAgent;
public class AgentWorker
{
- private const string Version = "2.5.0";
+ private const string Version = "2.6.0";
private const string DataDir = @"C:\ProgramData\IT Nexus Agent";
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
diff --git a/agent-cs/IT-Nexus-Agent.csproj b/agent-cs/IT-Nexus-Agent.csproj
index 8a9fc34..41454f7 100644
--- a/agent-cs/IT-Nexus-Agent.csproj
+++ b/agent-cs/IT-Nexus-Agent.csproj
@@ -7,8 +7,8 @@
true
IT-Nexus-Agent
ITNexusAgent
- 2.5.0
- 2.5.0.0
+ 2.6.0
+ 2.6.0.0
enable
enable
false
diff --git a/agent-cs/IndicatorModeRunner.cs b/agent-cs/IndicatorModeRunner.cs
new file mode 100644
index 0000000..11fd3a1
--- /dev/null
+++ b/agent-cs/IndicatorModeRunner.cs
@@ -0,0 +1,20 @@
+using ITNexusAgent.UI;
+
+namespace ITNexusAgent;
+
+// Läuft als User-Prozess (via SessionSpawner), zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay
+// solange RDP-Session aktiv ist. User kann selbst trennen (TCP-Signal an Service).
+public static class IndicatorModeRunner
+{
+ public static void Run(string portStr)
+ {
+ if (!int.TryParse(portStr, out var port) || port <= 0) return;
+
+ var app = new System.Windows.Application();
+ app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
+
+ var win = new RdpActiveIndicatorWindow(port);
+ win.Show();
+ app.Run();
+ }
+}
diff --git a/agent-cs/Program.cs b/agent-cs/Program.cs
index 1a16e6a..c036b43 100644
--- a/agent-cs/Program.cs
+++ b/agent-cs/Program.cs
@@ -29,6 +29,10 @@ internal class Program
args.Length > 2 && int.TryParse(args[2], out var si) ? si : 0);
return;
+ case "--rdp-indicator":
+ IndicatorModeRunner.Run(args.Length > 1 ? args[1] : "");
+ return;
+
case "--dashboard":
RunDashboard();
return;
diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs
index 356c630..5095e4c 100644
--- a/agent-cs/Services/RtcService.cs
+++ b/agent-cs/Services/RtcService.cs
@@ -15,6 +15,9 @@ public class RtcService
private readonly string _serverUrl;
private readonly string _agentKey;
private readonly string _hostname;
+ private int _indicatorPid = -1;
+ private TcpListener? _indicatorListener;
+ private CancellationTokenSource? _userDisconnectCts;
public RtcService(string serverUrl, string agentKey, string hostname)
{
@@ -76,21 +79,39 @@ public class RtcService
var newScreen = obj["screen"]?.ToObject() ?? 0;
captureCts.Cancel();
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
- _ = CapturePipeLoopAsync(ws, newScreen, captureCts.Token);
+ var switchToken = _userDisconnectCts != null
+ ? CancellationTokenSource.CreateLinkedTokenSource(captureCts.Token, _userDisconnectCts.Token).Token
+ : captureCts.Token;
+ _ = CapturePipeLoopAsync(ws, newScreen, switchToken);
AgentWorker.Log($"RDP: Screen gewechselt zu {newScreen}");
}
else if (type == "rdp_stop" && captureCts != null)
{
captureCts.Cancel();
captureCts = null;
+ StopIndicator();
AgentWorker.Log("RDP: Screen-Capture gestoppt");
}
}
captureCts?.Cancel();
+ StopIndicator();
AgentWorker.Log("RDP: Getrennt");
}
+ private void StopIndicator()
+ {
+ _userDisconnectCts?.Cancel();
+ _userDisconnectCts = null;
+ try { _indicatorListener?.Stop(); } catch { }
+ _indicatorListener = null;
+ if (_indicatorPid > 0)
+ {
+ try { Process.GetProcessById(_indicatorPid).Kill(); } catch { }
+ _indicatorPid = -1;
+ }
+ }
+
private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
{
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
@@ -136,7 +157,45 @@ public class RtcService
}
AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture");
- await CapturePipeLoopAsync(ws, screenIdx, ct);
+ SpawnIndicator(exePath);
+
+ var combinedToken = _userDisconnectCts != null
+ ? CancellationTokenSource.CreateLinkedTokenSource(ct, _userDisconnectCts.Token).Token
+ : ct;
+ await CapturePipeLoopAsync(ws, screenIdx, combinedToken);
+ }
+
+ // Zeigt dauerhaftes "Bildschirm wird übertragen"-Overlay (TeamViewer-Style) solange Capture läuft.
+ // User kann per Klick selbst trennen — Signal landet hier als CancellationTokenSource.Cancel().
+ private void SpawnIndicator(string exePath)
+ {
+ try
+ {
+ _indicatorListener = new TcpListener(IPAddress.Loopback, 0);
+ _indicatorListener.Start();
+ var port = ((IPEndPoint)_indicatorListener.LocalEndpoint).Port;
+
+ _userDisconnectCts = new CancellationTokenSource();
+ var listener = _indicatorListener;
+ var disconnectCts = _userDisconnectCts;
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ using var tcp = await listener.AcceptTcpClientAsync();
+ tcp.GetStream().ReadByte();
+ disconnectCts.Cancel();
+ AgentWorker.Log("RDP: User hat über Overlay getrennt");
+ }
+ catch { }
+ });
+
+ _indicatorPid = SessionSpawner.SpawnInUserSession(exePath, $"--rdp-indicator {port}");
+ }
+ catch (Exception ex)
+ {
+ AgentWorker.Log($"RDP: Indicator-Start fehlgeschlagen: {ex.Message}");
+ }
}
private static bool SpawnConsentHelper(string exePath, string portStr)
diff --git a/agent-cs/UI/RdpActiveIndicatorWindow.xaml b/agent-cs/UI/RdpActiveIndicatorWindow.xaml
new file mode 100644
index 0000000..5eaba6d
--- /dev/null
+++ b/agent-cs/UI/RdpActiveIndicatorWindow.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs b/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs
new file mode 100644
index 0000000..7036a7f
--- /dev/null
+++ b/agent-cs/UI/RdpActiveIndicatorWindow.xaml.cs
@@ -0,0 +1,55 @@
+using System.Net.Sockets;
+using System.Windows;
+using System.Windows.Media.Animation;
+
+namespace ITNexusAgent.UI;
+
+public partial class RdpActiveIndicatorWindow : Window
+{
+ private readonly int _port;
+ private readonly DateTime _startedAt = DateTime.Now;
+ private readonly System.Windows.Threading.DispatcherTimer _timer = new();
+ private bool _signaled = false;
+
+ public RdpActiveIndicatorWindow(int port)
+ {
+ InitializeComponent();
+ _port = port;
+
+ Loaded += (s, e) =>
+ {
+ var area = SystemParameters.WorkArea;
+ Left = area.Right - Width - 16;
+ Top = area.Bottom - Height - 16;
+ ((Storyboard)Resources["PulseAnim"]).Begin(PulseDot);
+ };
+
+ _timer.Interval = TimeSpan.FromSeconds(1);
+ _timer.Tick += (s, e) =>
+ {
+ var elapsed = DateTime.Now - _startedAt;
+ DurationText.Text = $"IT-Abteilung sieht zu · {elapsed:mm\\:ss}";
+ };
+ _timer.Start();
+ }
+
+ private void DisconnectButton_Click(object sender, RoutedEventArgs e)
+ {
+ SendDisconnectSignal();
+ Close();
+ }
+
+ private void SendDisconnectSignal()
+ {
+ if (_signaled) return;
+ _signaled = true;
+ try
+ {
+ using var tcp = new TcpClient();
+ tcp.Connect("127.0.0.1", _port);
+ tcp.GetStream().WriteByte(1);
+ tcp.GetStream().Flush();
+ }
+ catch { }
+ }
+}
diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss
index 3e91bde..028f2f0 100644
--- a/agent-cs/setup.iss
+++ b/agent-cs/setup.iss
@@ -1,5 +1,5 @@
#define MyAppName "IT Nexus Agent"
-#define MyAppVersion "2.5.0"
+#define MyAppVersion "2.6.0"
#define MyAppPublisher "Cereda Systems GmbH"
#define MyAppURL "https://it-nexus.cereda-systems.de"
#define MyAppExeName "IT-Nexus-Agent.exe"
diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx
index cc187c5..dd36158 100644
--- a/frontend/src/pages/PatchManagementPage.jsx
+++ b/frontend/src/pages/PatchManagementPage.jsx
@@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext';
const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } };
-const LATEST_AGENT_VERSION = '2.5.0';
+const LATEST_AGENT_VERSION = '2.6.0';
const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' };
const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' };
const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };