Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

109
agent-cs/AgentService.cs Normal file
View File

@@ -0,0 +1,109 @@
using System.ServiceProcess;
using System.Diagnostics;
namespace ITNexusAgent;
public class AgentService : ServiceBase
{
private CancellationTokenSource? _cts;
private Task? _worker;
public AgentService()
{
ServiceName = "IT Nexus Agent";
CanStop = true;
CanPauseAndContinue = false;
AutoLog = false;
}
protected override void OnStart(string[] args)
{
_cts = new CancellationTokenSource();
_worker = Task.Run(() => new AgentWorker(_cts.Token).RunAsync());
}
protected override void OnStop()
{
_cts?.Cancel();
_worker?.Wait(TimeSpan.FromSeconds(10));
}
public static void Install(string exePath)
{
// Bestehenden Service stoppen + löschen (falls vorhanden, damit binPath aktualisiert wird)
Run("net.exe", "stop \"IT Nexus Agent\"");
System.Threading.Thread.Sleep(2000);
Run("sc.exe", "delete \"IT Nexus Agent\"");
System.Threading.Thread.Sleep(1000);
// Dashboard-Prozesse beenden damit EXE nicht gesperrt ist
foreach (var p in System.Diagnostics.Process.GetProcessesByName("IT-Nexus-Agent"))
{
try { if (p.MainModule?.FileName != exePath) p.Kill(); } catch { }
}
// Neu erstellen mit korrektem Pfad
Run("sc.exe", $"create \"IT Nexus Agent\" binPath= \"{exePath}\" start= auto DisplayName= \"IT Nexus Agent\"");
Run("sc.exe", "description \"IT Nexus Agent\" \"Cereda Systems IT Nexus Monitoring Agent\"");
Run("sc.exe", "failure \"IT Nexus Agent\" reset= 60 actions= restart/5000/restart/10000/restart/30000");
Run("net.exe", "start \"IT Nexus Agent\"");
AgentWorker.Log($"Service installiert: {exePath}");
}
private static void Run(string exe, string args)
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe, args)
{ CreateNoWindow = true, UseShellExecute = false })?.WaitForExit();
}
catch { }
}
public static void Uninstall()
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("net.exe",
"stop \"IT Nexus Agent\"")
{ CreateNoWindow = true })?.WaitForExit();
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("sc.exe",
"delete \"IT Nexus Agent\"")
{ CreateNoWindow = true })?.WaitForExit();
AgentWorker.Log("Service deinstalliert");
}
// Migration vom alten PowerShell-Agent
public static void MigrateFromOldAgent()
{
try
{
var oldDataDir = @"C:\ProgramData\IT Nexus Agent";
var oldConfigPath = Path.Combine(oldDataDir, "config.json");
// Alte Scheduled Tasks entfernen
foreach (var taskName in new[] { "IT Nexus Agent", "IT Nexus Announcement Watcher" })
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("schtasks.exe",
$"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
}
// Alte PS1-Dateien bereinigen
if (Directory.Exists(oldDataDir))
{
foreach (var f in Directory.GetFiles(oldDataDir, "*.ps1"))
try { File.Delete(f); } catch { }
foreach (var f in Directory.GetFiles(oldDataDir, "*.update"))
try { File.Delete(f); } catch { }
}
AgentWorker.Log("Migration vom alten PowerShell-Agent abgeschlossen");
}
catch (Exception ex)
{
AgentWorker.Log($"Migration-Warnung: {ex.Message}");
}
}
}

195
agent-cs/AgentWorker.cs Normal file
View File

@@ -0,0 +1,195 @@
using ITNexusAgent.Models;
using ITNexusAgent.Services;
using Newtonsoft.Json;
namespace ITNexusAgent;
public class AgentWorker
{
private const string Version = "2.0.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";
private const string LogPath = @"C:\ProgramData\IT Nexus Agent\agent.log";
private readonly CancellationToken _ct;
private AgentConfig? _config;
private ApiService? _api;
private NotificationService? _notifier;
private string _exePath = "";
public AgentWorker(CancellationToken ct) => _ct = ct;
public async Task RunAsync()
{
_exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
Directory.CreateDirectory(DataDir);
if (!File.Exists(ConfigPath))
{
Log("ERROR: config.json nicht gefunden");
return;
}
SecureConfigFile(ConfigPath);
_config = AgentConfig.Load(ConfigPath);
_api = new ApiService(_config.ServerUrl, _config.AgentKey);
_notifier = new NotificationService(_exePath, DataDir);
Log($"Agent v{Version} gestartet");
while (!_ct.IsCancellationRequested)
{
await RunCycleAsync();
await Task.Delay(TimeSpan.FromMinutes(1), _ct).ContinueWith(_ => { });
}
}
private async Task RunCycleAsync()
{
try
{
Log("Sammle Systemdaten...");
var payload = CollectData();
var response = await _api!.CheckinAsync(payload);
Log($"OK: Checkin erfolgreich (Hostname: {payload.Hostname})");
// Status-Cache für Dashboard schreiben
WriteStatusCache(payload, response, true);
// Auto-Update prüfen
if (!string.IsNullOrEmpty(response.AgentVersion) &&
System.Version.TryParse(response.AgentVersion, out var serverVer) &&
System.Version.TryParse(Version, out var localVer) &&
serverVer > localVer)
{
Log($"UPDATE: Neue Agent-Version verfügbar: {response.AgentVersion} (aktuell: {Version})");
await new CommandExecutor(_api, payload.Hostname, DataDir, _exePath)
.ExecuteAsync(new PatchCommand { Id = 0, Command = "update_agent" });
return;
}
// Patch-Commands ausführen
var executor = new CommandExecutor(_api, payload.Hostname, DataDir, _exePath);
foreach (var cmd in response.Commands)
{
Log($"PATCH: Command empfangen: {cmd.Command}");
await executor.ExecuteAsync(cmd);
}
// Ankündigungen anzeigen
if (response.Announcements.Count > 0)
{
_notifier!.ShowAnnouncements(response.Announcements);
foreach (var ann in response.Announcements)
Log($"ANNOUNCEMENT: Dialog für {SystemInfoService.GetLastUser()} gestartet - ID {ann.Id}");
}
}
catch (Exception ex)
{
Log($"ERROR: {ex.Message}");
WriteStatusCache(null, null, false);
}
}
private CheckinPayload CollectData()
{
var (cpuModel, cpuCores) = SystemInfoService.GetCpuInfo();
var (ramTotal, ramUsed) = SystemInfoService.GetRamInfo();
var (diskTotal, diskFree) = SystemInfoService.GetDiskInfo();
var (tpmPresent, tpmVersion, tpmV2) = SystemInfoService.GetTpmInfo();
var (defEnabled, defSigAge, _) = SecurityInfoService.GetDefenderStatus();
return new CheckinPayload
{
Hostname = SystemInfoService.GetHostname(),
AgentVersion = Version,
Os = SystemInfoService.GetOs(),
OsVersion = SystemInfoService.GetOsVersion(),
Domain = SystemInfoService.GetDomain(),
IpAddress = SystemInfoService.GetIpAddress(),
MacAddress = SystemInfoService.GetMacAddress(),
LastUser = SystemInfoService.GetLastUser(),
CpuModel = cpuModel,
CpuCores = cpuCores,
CpuUsage = SystemInfoService.GetCpuUsage(),
RamTotal = ramTotal,
RamUsed = ramUsed,
DiskTotal = diskTotal,
DiskFree = diskFree,
UptimeHours = SystemInfoService.GetUptimeHours(),
PendingUpdates = SystemInfoService.GetPendingUpdates(),
TpmPresent = tpmPresent,
TpmVersion = tpmVersion,
TpmV2 = tpmV2,
SecureBoot = SystemInfoService.GetSecureBoot(),
Win11Ready = SystemInfoService.GetWin11Readiness(tpmV2, SystemInfoService.GetSecureBoot(), cpuCores, ramTotal),
BitlockerStatus = SecurityInfoService.GetBitlockerStatus(),
DefenderEnabled = defEnabled,
DefenderSignaturesAge = defSigAge >= 0 ? defSigAge : null,
HardwareSerial = SystemInfoService.GetHardwareSerial(),
InstalledSoftware = SystemInfoService.GetInstalledSoftware(),
};
}
private void WriteStatusCache(CheckinPayload? p, CheckinResponse? r, bool online)
{
try
{
var status = new StatusCache
{
Hostname = p?.Hostname ?? SystemInfoService.GetHostname(),
LastCheckin = DateTime.Now,
AgentVersion = Version,
ServerUrl = _config?.ServerUrl ?? "",
Online = online,
CpuUsage = p?.CpuUsage,
RamTotal = p?.RamTotal,
RamUsed = p?.RamUsed,
DiskTotal = p?.DiskTotal,
DiskFree = p?.DiskFree,
PendingUpdates = p?.PendingUpdates ?? 0,
BitlockerStatus = p?.BitlockerStatus ?? "",
DefenderEnabled = p?.DefenderEnabled ?? false,
DefenderSignaturesAge = p?.DefenderSignaturesAge,
HardwareSerial = p?.HardwareSerial ?? "",
Os = p?.Os ?? "",
LastUser = p?.LastUser ?? "",
InstalledSoftware = p?.InstalledSoftware ?? [],
};
File.WriteAllText(StatusPath, JsonConvert.SerializeObject(status, Formatting.Indented));
}
catch { }
}
private static void SecureConfigFile(string path)
{
try
{
var fi = new System.IO.FileInfo(path);
var acl = fi.GetAccessControl();
acl.SetAccessRuleProtection(true, false); // Vererbung entfernen
// Nur SYSTEM und Administratoren
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
"SYSTEM", System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow));
acl.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
"Administrators", System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow));
fi.SetAccessControl(acl);
}
catch { }
}
public static void Log(string msg)
{
try
{
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}";
File.AppendAllText(LogPath, line + Environment.NewLine, System.Text.Encoding.UTF8);
}
catch { }
}
}

8
agent-cs/GlobalUsings.cs Normal file
View File

@@ -0,0 +1,8 @@
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>IT-Nexus-Agent</AssemblyName>
<RootNamespace>ITNexusAgent</RootNamespace>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Resource Include="icon.ico"/>
<Content Include="icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,91 @@
using Newtonsoft.Json;
namespace ITNexusAgent.Models;
public class CheckinPayload
{
[JsonProperty("hostname")] public string Hostname { get; set; } = "";
[JsonProperty("agent_version")] public string AgentVersion { get; set; } = "";
[JsonProperty("os")] public string Os { get; set; } = "";
[JsonProperty("os_version")] public string OsVersion { get; set; } = "";
[JsonProperty("domain")] public string Domain { get; set; } = "";
[JsonProperty("ip_address")] public string IpAddress { get; set; } = "";
[JsonProperty("mac_address")] public string MacAddress { get; set; } = "";
[JsonProperty("last_user")] public string LastUser { get; set; } = "";
[JsonProperty("cpu_model")] public string CpuModel { get; set; } = "";
[JsonProperty("cpu_cores")] public int CpuCores { get; set; }
[JsonProperty("cpu_usage")] public double? CpuUsage { get; set; }
[JsonProperty("ram_total")] public double? RamTotal { get; set; }
[JsonProperty("ram_used")] public double? RamUsed { get; set; }
[JsonProperty("disk_total")] public double? DiskTotal { get; set; }
[JsonProperty("disk_free")] public double? DiskFree { get; set; }
[JsonProperty("uptime_hours")] public double? UptimeHours { get; set; }
[JsonProperty("pending_updates")] public int PendingUpdates { get; set; }
[JsonProperty("tpm_present")] public bool TpmPresent { get; set; }
[JsonProperty("tpm_version")] public string TpmVersion { get; set; } = "";
[JsonProperty("tpm_v2")] public bool TpmV2 { get; set; }
[JsonProperty("secure_boot")] public bool SecureBoot { get; set; }
[JsonProperty("win11_ready")] public bool Win11Ready { get; set; }
[JsonProperty("bitlocker_status")] public string BitlockerStatus { get; set; } = "";
[JsonProperty("defender_enabled")] public bool DefenderEnabled { get; set; }
[JsonProperty("defender_signatures_age")] public int? DefenderSignaturesAge { get; set; }
[JsonProperty("hardware_serial")] public string HardwareSerial { get; set; } = "";
[JsonProperty("installed_software")] public List<string> InstalledSoftware { get; set; } = [];
}
public class CheckinResponse
{
[JsonProperty("agent_version")] public string? AgentVersion { get; set; }
[JsonProperty("commands")] public List<PatchCommand> Commands { get; set; } = [];
[JsonProperty("announcements")] public List<Announcement> Announcements { get; set; } = [];
[JsonProperty("running_commands")] public List<RunningCommand> RunningCommands { get; set; } = [];
}
public class PatchCommand
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("command")] public string Command { get; set; } = "";
}
public class RunningCommand
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("command")] public string Command { get; set; } = "";
}
public class Announcement
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("title")] public string Title { get; set; } = "";
[JsonProperty("message")] public string Message { get; set; } = "";
[JsonProperty("type")] public string Type { get; set; } = "info";
}
public class CommandResult
{
[JsonProperty("command_id")] public int CommandId { get; set; }
[JsonProperty("status")] public string Status { get; set; } = "done";
[JsonProperty("result")] public string Result { get; set; } = "";
}
public class StatusCache
{
[JsonProperty("hostname")] public string Hostname { get; set; } = "";
[JsonProperty("last_checkin")] public DateTime LastCheckin { get; set; }
[JsonProperty("agent_version")] public string AgentVersion { get; set; } = "";
[JsonProperty("server_url")] public string ServerUrl { get; set; } = "";
[JsonProperty("online")] public bool Online { get; set; }
[JsonProperty("cpu_usage")] public double? CpuUsage { get; set; }
[JsonProperty("ram_total")] public double? RamTotal { get; set; }
[JsonProperty("ram_used")] public double? RamUsed { get; set; }
[JsonProperty("disk_total")] public double? DiskTotal { get; set; }
[JsonProperty("disk_free")] public double? DiskFree { get; set; }
[JsonProperty("pending_updates")] public int PendingUpdates { get; set; }
[JsonProperty("bitlocker_status")] public string BitlockerStatus { get; set; } = "";
[JsonProperty("defender_enabled")] public bool DefenderEnabled { get; set; }
[JsonProperty("defender_sig_age")] public int? DefenderSignaturesAge { get; set; }
[JsonProperty("hardware_serial")] public string HardwareSerial { get; set; } = "";
[JsonProperty("os")] public string Os { get; set; } = "";
[JsonProperty("last_user")] public string LastUser { get; set; } = "";
[JsonProperty("installed_software")] public List<string> InstalledSoftware { get; set; } = [];
}

20
agent-cs/Models/Config.cs Normal file
View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
using System.IO;
namespace ITNexusAgent.Models;
public class AgentConfig
{
[JsonProperty("server_url")]
public string ServerUrl { get; set; } = "";
[JsonProperty("agent_key")]
public string AgentKey { get; set; } = "";
public static AgentConfig Load(string path)
{
var json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<AgentConfig>(json)
?? throw new Exception("Ungültige config.json");
}
}

58
agent-cs/Program.cs Normal file
View File

@@ -0,0 +1,58 @@
using ITNexusAgent;
using ITNexusAgent.Models;
using ITNexusAgent.UI;
using Newtonsoft.Json;
using System.ServiceProcess;
internal class Program
{
[STAThread]
static void Main(string[] args)
{
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
if (args.Length > 0)
{
switch (args[0])
{
case "--notify":
RunNotification(args.Length > 1 ? args[1] : "");
return;
case "--dashboard":
RunDashboard();
return;
case "--install":
AgentService.MigrateFromOldAgent();
AgentService.Install(exePath);
return;
case "--uninstall":
AgentService.Uninstall();
return;
}
}
ServiceBase.Run(new AgentService());
}
static void RunNotification(string base64Json)
{
try
{
var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64Json));
var ann = JsonConvert.DeserializeObject<Announcement>(json);
if (ann == null) return;
var app = new System.Windows.Application();
app.Run(new NotificationWindow(ann));
}
catch { }
}
static void RunDashboard()
{
var app = new System.Windows.Application();
app.Run(new DashboardWindow());
}
}

View File

@@ -0,0 +1,89 @@
using ITNexusAgent.Models;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
namespace ITNexusAgent.Services;
public class ApiService(string serverUrl, string agentKey)
{
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
private readonly string _baseUrl = serverUrl.TrimEnd('/');
private readonly string _agentKey = agentKey;
private HttpRequestMessage BuildRequest(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, $"{_baseUrl}{path}");
req.Headers.Add("X-Agent-Key", _agentKey);
if (body != null)
{
var json = JsonConvert.SerializeObject(body);
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return req;
}
public async Task<CheckinResponse> CheckinAsync(CheckinPayload payload)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/checkin", payload);
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CheckinResponse>(body) ?? new CheckinResponse();
}
public async Task<List<Announcement>> PollAnnouncementsAsync(string hostname)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/announcements-poll",
new { hostname });
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeAnonymousType(body, new { announcements = new List<Announcement>() });
return result?.announcements ?? [];
}
public async Task AckAnnouncementAsync(int id, string hostname)
{
var req = BuildRequest(HttpMethod.Post, $"/api/announcements/{id}/ack-agent",
new { hostname });
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
}
public async Task ReportCommandResultAsync(CommandResult result)
{
var req = BuildRequest(HttpMethod.Post, "/api/patch/commands/result", result);
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
}
public async Task<byte[]> DownloadAgentAsync()
{
var req = BuildRequest(HttpMethod.Get, "/api/monitoring/agent-script");
var resp = await _http.SendAsync(req);
return await resp.Content.ReadAsByteArrayAsync();
}
public async Task<byte[]> DownloadSetupAsync()
{
var req = BuildRequest(HttpMethod.Get, "/api/monitoring/agent-setup");
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsByteArrayAsync();
}
public async Task SendMessageAsync(string hostname, string message)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/message",
new { hostname, message });
await _http.SendAsync(req);
}
public async Task SendRebootRequestAsync(string hostname)
{
var req = BuildRequest(HttpMethod.Post, "/api/monitoring/reboot-request",
new { hostname });
await _http.SendAsync(req);
}
}

View File

@@ -0,0 +1,123 @@
using ITNexusAgent.Models;
using System.Diagnostics;
using System.Management;
using System.Net.Http;
namespace ITNexusAgent.Services;
public class CommandExecutor(ApiService api, string hostname, string dataDir, string exePath)
{
private readonly ApiService _api = api;
private readonly string _hostname = hostname;
private readonly string _dataDir = dataDir;
private readonly string _exePath = exePath;
private static void RunSchtasks(string args) =>
Process.Start(new ProcessStartInfo("schtasks.exe", args) { CreateNoWindow = true })?.WaitForExit();
public async Task ExecuteAsync(PatchCommand cmd)
{
string result;
try
{
result = cmd.Command switch
{
"check_updates" => await CheckUpdates(),
"install_updates" => await InstallUpdates(),
"reboot" => await Reboot(cmd.Id),
"upgrade_win11" => await UpgradeWin11(),
"update_agent" => await UpdateAgent(),
_ => $"Unbekannter Command: {cmd.Command}"
};
}
catch (Exception ex)
{
result = $"Fehler: {ex.Message}";
}
if (cmd.Command != "reboot")
{
await _api.ReportCommandResultAsync(new CommandResult
{
CommandId = cmd.Id,
Status = "done",
Result = result
});
}
}
private Task<string> CheckUpdates()
{
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartScan") { CreateNoWindow = true });
Thread.Sleep(5000);
return Task.FromResult("Update-Scan gestartet");
}
private Task<string> InstallUpdates()
{
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartDownload") { CreateNoWindow = true });
Thread.Sleep(3000);
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartInstall") { CreateNoWindow = true });
return Task.FromResult("Update-Installation gestartet");
}
private async Task<string> Reboot(int cmdId)
{
await _api.ReportCommandResultAsync(new CommandResult
{
CommandId = cmdId,
Status = "done",
Result = "Neustart wird in 60 Sekunden durchgeführt"
});
Process.Start("shutdown.exe", "/r /t 60 /c \"IT Nexus Patch Management - Geplanter Neustart\"");
return "";
}
private Task<string> UpgradeWin11()
{
var exePath = Path.Combine(_dataDir, "Win11Upgrade.exe");
using var client = new HttpClient();
var bytes = client.GetByteArrayAsync("https://go.microsoft.com/fwlink/?linkid=2171764").Result;
if (bytes.Length < 1024 * 1024) return Task.FromResult("Download fehlgeschlagen");
File.WriteAllBytes(exePath, bytes);
var fullUser = NotificationService.GetLoggedOnUser();
if (!string.IsNullOrEmpty(fullUser))
{
RunSchtasks("/delete /tn \"IT Nexus Win11 Upgrade\" /f");
RunSchtasks($"/create /tn \"IT Nexus Win11 Upgrade\" /tr \"\\\"{exePath}\\\" /skipeula /auto upgrade\" /sc ONCE /st {DateTime.Now.AddSeconds(20):HH:mm} /ru \"{fullUser}\" /it /rl HIGHEST /f");
}
else
{
Process.Start(new ProcessStartInfo(exePath, "/quietinstall /skipeula /auto upgrade")
{ CreateNoWindow = true });
}
return Task.FromResult("running");
}
private async Task<string> UpdateAgent()
{
var bytes = await _api.DownloadSetupAsync();
if (bytes.Length < 512 * 1024) return "Download fehlgeschlagen - Installer zu klein";
var setupPath = Path.Combine(_dataDir, "IT-Nexus-Agent-Setup-Update.exe");
var batchPath = Path.Combine(_dataDir, "update.cmd");
await File.WriteAllBytesAsync(setupPath, bytes);
// Batch: wartet kurz, führt Installer silent aus, löscht sich selbst
var batch = $"""
@echo off
timeout /t 5 /nobreak > nul
"{setupPath}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
del "{setupPath}" > nul
del "%~f0"
""";
await File.WriteAllTextAsync(batchPath, batch, System.Text.Encoding.ASCII);
Process.Start(new ProcessStartInfo("cmd.exe", $"/c \"{batchPath}\"")
{ CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden });
return "Update-Installer gestartet";
}
}

View File

@@ -0,0 +1,110 @@
using ITNexusAgent.Models;
using Newtonsoft.Json;
using System.Diagnostics;
using System.Management;
using System.Text;
namespace ITNexusAgent.Services;
public class NotificationService
{
private readonly string _exePath;
private readonly string _dataDir;
private readonly string _shownIdsPath;
private readonly HashSet<int> _shownIds;
public NotificationService(string exePath, string dataDir)
{
_exePath = exePath;
_dataDir = dataDir;
_shownIdsPath = Path.Combine(dataDir, "shown_announcements.json");
_shownIds = LoadShownIds();
}
private HashSet<int> LoadShownIds()
{
try
{
if (File.Exists(_shownIdsPath))
{
var ids = JsonConvert.DeserializeObject<List<int>>(File.ReadAllText(_shownIdsPath));
return ids != null ? new HashSet<int>(ids) : [];
}
}
catch { }
return [];
}
private void SaveShownIds()
{
try { File.WriteAllText(_shownIdsPath, JsonConvert.SerializeObject(_shownIds.ToList())); }
catch { }
}
public void ShowAnnouncements(List<Announcement> announcements)
{
foreach (var ann in announcements)
{
if (_shownIds.Contains(ann.Id)) continue;
_shownIds.Add(ann.Id);
SaveShownIds();
ShowNotification(ann);
}
}
// Vollständigen eingeloggten User aus WMI holen (z.B. "WINKEL\gruessing" oder "AzureAD\gruessing")
public static string GetLoggedOnUser()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT UserName FROM Win32_ComputerSystem");
foreach (ManagementObject obj in searcher.Get())
{
var user = obj["UserName"]?.ToString();
if (!string.IsNullOrEmpty(user)) return user;
}
}
catch { }
return "";
}
private void ShowNotification(Announcement ann)
{
try
{
var fullUser = GetLoggedOnUser();
if (string.IsNullOrEmpty(fullUser)) return;
var json = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(ann)));
var taskName = $"ITNexus-Ann-{ann.Id}";
// Alten Task entfernen
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
// Task mit Trigger weit in der Zukunft erstellen (damit er nicht abläuft vor /run)
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{_exePath}\\\" --notify {json}\" " +
$"/sc ONCE /st {triggerTime} /ru \"{fullUser}\" /it /f";
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
p?.WaitForExit();
// Sofort ausführen
var run = Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
{ CreateNoWindow = true });
run?.WaitForExit();
// Task löschen damit der +60min Trigger nicht nochmal feuert
Thread.Sleep(3000);
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
{ CreateNoWindow = true })?.WaitForExit();
AgentWorker.Log($"NOTIFY: Task '{taskName}' für User '{fullUser}' gestartet (ExitCode={p?.ExitCode})");
}
catch (Exception ex)
{
AgentWorker.Log($"NOTIFY ERROR: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Management;
namespace ITNexusAgent.Services;
public static class SecurityInfoService
{
public static string GetBitlockerStatus()
{
try
{
var scope = new ManagementScope(@"\\.\root\cimv2\Security\MicrosoftVolumeEncryption");
scope.Connect();
using var searcher = new ManagementObjectSearcher(scope,
new System.Management.ObjectQuery("SELECT * FROM Win32_EncryptableVolume WHERE DriveLetter='C:'"));
foreach (ManagementObject obj in searcher.Get())
{
var protection = Convert.ToInt32(obj["ProtectionStatus"]);
return protection switch
{
1 => "encrypted",
2 => "unknown",
_ => "off"
};
}
}
catch { }
return "unknown";
}
public static (bool Enabled, int SignaturesAgeDays, DateTime? LastScan) GetDefenderStatus()
{
try
{
var scope = new ManagementScope(@"\\.\root\Microsoft\Windows\Defender");
scope.Connect();
using var searcher = new ManagementObjectSearcher(scope,
new System.Management.ObjectQuery("SELECT * FROM MSFT_MpComputerStatus"));
foreach (ManagementObject obj in searcher.Get())
{
var enabled = Convert.ToBoolean(obj["AMServiceEnabled"] ?? false);
var sigDate = obj["AntivirusSignatureLastUpdated"]?.ToString();
var lastScanAge = Convert.ToInt32(obj["QuickScanAge"] ?? 999);
int sigAge = 0;
if (!string.IsNullOrEmpty(sigDate) && sigDate.Length >= 8)
{
if (DateTime.TryParseExact(sigDate[..14], "yyyyMMddHHmmss",
null, System.Globalization.DateTimeStyles.None, out var sigDateTime))
{
sigAge = (int)(DateTime.Now - sigDateTime).TotalDays;
}
}
DateTime? lastScan = lastScanAge < 999
? DateTime.Now.AddDays(-lastScanAge)
: null;
return (enabled, sigAge, lastScan);
}
}
catch { }
return (false, -1, null);
}
}

View File

@@ -0,0 +1,267 @@
using ITNexusAgent.Models;
using System.Diagnostics;
using System.Management;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.Win32;
namespace ITNexusAgent.Services;
public static class SystemInfoService
{
public static string GetHostname() => Environment.MachineName;
public static string GetDomain() =>
Environment.UserDomainName != Environment.MachineName
? Environment.UserDomainName
: "";
public static string GetOs()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
var name = key?.GetValue("ProductName")?.ToString() ?? "Windows";
var build = key?.GetValue("CurrentBuildNumber")?.ToString();
var ubr = key?.GetValue("UBR")?.ToString();
// Build >= 22000 = Windows 11 — Registry ProductName kann noch "Windows 10" sagen nach Upgrade
if (build != null && int.TryParse(build, out var buildNum) && buildNum >= 22000)
name = name.Replace("Windows 10", "Windows 11");
return build != null ? $"{name} (Build {build}.{ubr})" : name;
}
catch { return RuntimeInformation.OSDescription; }
}
public static string GetOsVersion()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
return key?.GetValue("CurrentBuildNumber")?.ToString() ?? Environment.OSVersion.Version.ToString();
}
catch { return Environment.OSVersion.Version.ToString(); }
}
public static string GetIpAddress()
{
try
{
foreach (var iface in NetworkInterface.GetAllNetworkInterfaces())
{
if (iface.OperationalStatus != OperationalStatus.Up) continue;
if (iface.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
var name = iface.Name.ToLower();
if (name.Contains("virtual") || name.Contains("wsl") || name.Contains("vmware")) continue;
foreach (var addr in iface.GetIPProperties().UnicastAddresses)
{
if (addr.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue;
var ip = addr.Address.ToString();
if (ip.StartsWith("169.254") || ip.StartsWith("172.")) continue;
return ip;
}
}
}
catch { }
return "";
}
public static string GetMacAddress()
{
try
{
foreach (var iface in NetworkInterface.GetAllNetworkInterfaces())
{
if (iface.OperationalStatus != OperationalStatus.Up) continue;
if (iface.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
var mac = iface.GetPhysicalAddress().ToString();
if (mac.Length == 12)
return string.Join(":", Enumerable.Range(0, 6).Select(i => mac.Substring(i * 2, 2)));
}
}
catch { }
return "";
}
public static string GetLastUser()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT UserName FROM Win32_ComputerSystem");
foreach (ManagementObject obj in searcher.Get())
{
var user = obj["UserName"]?.ToString();
if (!string.IsNullOrEmpty(user))
return user.Contains('\\') ? user.Split('\\')[1] : user;
}
// Fallback: letztes Profil
using var key = Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI");
return key?.GetValue("LastLoggedOnUser")?.ToString()?.Split('\\').LastOrDefault() ?? "";
}
catch { return ""; }
}
public static (string Model, int Cores) GetCpuInfo()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT Name, NumberOfCores FROM Win32_Processor");
foreach (ManagementObject obj in searcher.Get())
return (obj["Name"]?.ToString()?.Trim() ?? "", Convert.ToInt32(obj["NumberOfCores"]));
}
catch { }
return ("", 0);
}
public static double GetCpuUsage()
{
try
{
using var counter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
counter.NextValue();
Thread.Sleep(800);
return Math.Round(counter.NextValue(), 1);
}
catch { return 0; }
}
public static (double Total, double Used) GetRamInfo()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize, FreePhysicalMemory FROM Win32_OperatingSystem");
foreach (ManagementObject obj in searcher.Get())
{
var total = Math.Round(Convert.ToDouble(obj["TotalVisibleMemorySize"]) / (1024 * 1024), 2);
var free = Math.Round(Convert.ToDouble(obj["FreePhysicalMemory"]) / (1024 * 1024), 2);
return (total, Math.Round(total - free, 2));
}
}
catch { }
return (0, 0);
}
public static (double Total, double Free) GetDiskInfo()
{
try
{
var drive = new DriveInfo("C");
return (
Math.Round(drive.TotalSize / (double)(1024 * 1024 * 1024), 2),
Math.Round(drive.AvailableFreeSpace / (double)(1024 * 1024 * 1024), 2)
);
}
catch { return (0, 0); }
}
public static double GetUptimeHours()
{
try
{
var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);
return Math.Round(uptime.TotalHours, 1);
}
catch { return 0; }
}
public static int GetPendingUpdates()
{
try
{
var type = Type.GetTypeFromProgID("Microsoft.Update.Session");
if (type == null) return 0;
dynamic session = Activator.CreateInstance(type)!;
var searcher = session.CreateUpdateSearcher();
var result = searcher.Search("IsInstalled=0 and Type='Software'");
return result.Updates.Count;
}
catch { return 0; }
}
public static (bool Present, string Version, bool V2) GetTpmInfo()
{
try
{
using var searcher = new ManagementObjectSearcher(@"root\CIMv2\Security\MicrosoftTpm",
"SELECT * FROM Win32_Tpm");
foreach (ManagementObject obj in searcher.Get())
{
var version = obj["SpecVersion"]?.ToString() ?? "";
return (true, version, version.StartsWith("2.0"));
}
}
catch { }
return (false, "", false);
}
public static bool GetSecureBoot()
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Control\SecureBoot\State");
return Convert.ToInt32(key?.GetValue("UEFISecureBootEnabled") ?? 0) == 1;
}
catch { return false; }
}
public static bool GetWin11Readiness(bool tpmV2, bool secureBoot, int cpuCores, double ramGb)
{
// Wenn bereits Win 11 läuft → immer true
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
var build = key?.GetValue("CurrentBuildNumber")?.ToString();
if (build != null && int.TryParse(build, out var b) && b >= 22000) return true;
}
catch { }
return tpmV2 && secureBoot && cpuCores >= 2 && ramGb >= 4;
}
public static string GetHardwareSerial()
{
try
{
using var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BIOS");
foreach (ManagementObject obj in searcher.Get())
return obj["SerialNumber"]?.ToString()?.Trim() ?? "";
}
catch { }
return "";
}
public static List<string> GetInstalledSoftware()
{
var apps = new HashSet<string>();
var paths = new[]
{
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
};
foreach (var path in paths)
{
try
{
using var key = Registry.LocalMachine.OpenSubKey(path);
if (key == null) continue;
foreach (var subName in key.GetSubKeyNames())
{
using var sub = key.OpenSubKey(subName);
var name = sub?.GetValue("DisplayName")?.ToString();
if (string.IsNullOrWhiteSpace(name)) continue;
if (System.Text.RegularExpressions.Regex.IsMatch(name, @"^KB\d+")) continue;
apps.Add(name.Trim());
}
}
catch { }
}
return [.. apps.OrderBy(x => x).Take(100)];
}
}

View File

@@ -0,0 +1,28 @@
<Window x:Class="ITNexusAgent.UI.AdminLoginDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Admin-Login"
Width="360" Height="230"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize" Background="White" FontFamily="Segoe UI">
<StackPanel Margin="24">
<TextBlock Text="Admin-Anmeldung" FontSize="14" FontWeight="Bold"
Foreground="#1F2937" Margin="0,0,0,16"/>
<TextBlock Text="Benutzername" FontSize="11" Foreground="#6B7280" Margin="0,0,0,4"/>
<TextBox x:Name="UsernameBox" Height="32" Padding="8,4" FontSize="12"
BorderBrush="#D1D5DB" Margin="0,0,0,10"/>
<TextBlock Text="Passwort" FontSize="11" Foreground="#6B7280" Margin="0,0,0,4"/>
<PasswordBox x:Name="PasswordBox" Height="32" Padding="8,4" FontSize="12"
BorderBrush="#D1D5DB" Margin="0,0,0,4"
KeyDown="PasswordBox_KeyDown"/>
<TextBlock x:Name="ErrorLabel" FontSize="11" Foreground="#EF4444"
Margin="0,4,0,0" Visibility="Collapsed"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
<Button Content="Abbrechen" Width="80" Height="32" Margin="0,0,8,0"
Click="Cancel_Click" Background="#F9FAFB" BorderBrush="#D1D5DB" FontSize="12"/>
<Button x:Name="LoginButton" Content="Anmelden" Width="90" Height="32"
Click="Login_Click" Background="#0078D4" Foreground="White"
BorderThickness="0" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
</StackPanel>
</Window>

View File

@@ -0,0 +1,65 @@
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
using System.Windows;
using System.Windows.Input;
namespace ITNexusAgent.UI;
public partial class AdminLoginDialog : Window
{
public string Token { get; private set; } = "";
public string Username { get; private set; } = "";
private readonly string _serverUrl;
public AdminLoginDialog(string serverUrl)
{
InitializeComponent();
_serverUrl = serverUrl.TrimEnd('/');
}
private async void Login_Click(object sender, RoutedEventArgs e)
{
var username = UsernameBox.Text.Trim();
var password = PasswordBox.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return;
LoginButton.IsEnabled = false;
ErrorLabel.Visibility = Visibility.Collapsed;
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var body = JsonConvert.SerializeObject(new { username, password });
var resp = await http.PostAsync($"{_serverUrl}/api/auth/login",
new StringContent(body, Encoding.UTF8, "application/json"));
var json = await resp.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeAnonymousType(json, new { token = "" });
if (resp.IsSuccessStatusCode && !string.IsNullOrEmpty(result?.token))
{
Token = result.token;
Username = username;
DialogResult = true;
}
else
{
ErrorLabel.Text = "Ungültige Anmeldedaten";
ErrorLabel.Visibility = Visibility.Visible;
}
}
catch
{
ErrorLabel.Text = "Verbindung fehlgeschlagen";
ErrorLabel.Visibility = Visibility.Visible;
}
finally { LoginButton.IsEnabled = true; }
}
private void PasswordBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Enter) Login_Click(sender, new RoutedEventArgs());
}
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
}

View File

@@ -0,0 +1,510 @@
<Window x:Class="ITNexusAgent.UI.DashboardWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Agent"
Icon="pack://application:,,,/icon.ico"
Width="560" Height="640"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize"
StateChanged="Window_StateChanged"
Background="Transparent"
FontFamily="Segoe UI"
WindowStyle="None"
AllowsTransparency="True"
MinWidth="480" MinHeight="540">
<Window.Resources>
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="#1C1C1F"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="16,14"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="BtnAction" TargetType="Button">
<Setter Property="Background" Value="#2A2A2E"/>
<Setter Property="Foreground" Value="#E4E4E7"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="9"
BorderThickness="1" BorderBrush="#333337">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#35353A"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="#4A4A50"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1A1A1D"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BtnPrimary" TargetType="Button" BasedOn="{StaticResource BtnAction}">
<Setter Property="Background" Value="#1D4ED8"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="9" BorderThickness="0">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2563EB"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1E40AF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BtnWarn" TargetType="Button" BasedOn="{StaticResource BtnAction}">
<Setter Property="Background" Value="#27180A"/>
<Setter Property="Foreground" Value="#F97316"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="9"
BorderThickness="1" BorderBrush="#3D2510">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#352010"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BtnGhost" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#52525B"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<TextBlock x:Name="T" Text="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
HorizontalAlignment="Center"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="T" Property="Foreground" Value="#71717A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="BtnAdminAction" TargetType="Button" BasedOn="{StaticResource BtnAction}">
<Setter Property="FontSize" Value="11"/>
</Style>
</Window.Resources>
<!-- Outer container -->
<Grid>
<Border x:Name="OuterBorder" Background="#111113" CornerRadius="14" BorderBrush="#2A2A2E" BorderThickness="1"
Margin="16">
<Border.Effect>
<DropShadowEffect Color="Black" Opacity="0.4" BlurRadius="20" ShadowDepth="0"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ═══ TITLEBAR (draggable) ═══ -->
<Border x:Name="TitleBarBorder" Grid.Row="0" Background="#18181B" CornerRadius="14,14,0,0"
MouseLeftButtonDown="TitleBar_MouseDown">
<Grid Margin="18,14,18,14">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="IT Nexus Agent" FontSize="15" FontWeight="Bold"
Foreground="#FAFAFA"/>
<TextBlock x:Name="HostnameLabel" FontSize="11" Foreground="#52525B"
Margin="0,2,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<!-- Status -->
<StackPanel Orientation="Horizontal" Margin="0,0,20,0" VerticalAlignment="Center">
<Ellipse x:Name="StatusDot" Width="8" Height="8" Margin="0,0,7,0" VerticalAlignment="Center"/>
<TextBlock x:Name="StatusLabel" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
<!-- Minimieren -->
<Button Width="32" Height="32" Click="Minimize_Click" ToolTip="Minimieren">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="Transparent" CornerRadius="6">
<TextBlock Text="&#xE921;" FontFamily="Segoe MDL2 Assets"
FontSize="9" Foreground="#52525B"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2A2A2E"/>
<Setter Property="Foreground" Value="#A1A1AA"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<!-- Maximieren -->
<Button Width="32" Height="32" Click="Maximize_Click" ToolTip="Maximieren">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="Transparent" CornerRadius="6">
<TextBlock x:Name="MaxIcon" Text="&#xE922;" FontFamily="Segoe MDL2 Assets"
FontSize="9" Foreground="#52525B"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2A2A2E"/>
<Setter TargetName="MaxIcon" Property="Foreground" Value="#A1A1AA"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<!-- Schließen -->
<Button Width="32" Height="32" Click="Close_Click" ToolTip="Schließen">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="Transparent" CornerRadius="6">
<TextBlock Text="&#xE711;" FontFamily="Segoe MDL2 Assets"
FontSize="9" Foreground="#52525B"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#C0392B"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- ═══ CONTENT ═══ -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden"
Background="#111113">
<StackPanel Margin="14,12,14,4">
<!-- Metric Cards -->
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="#1C1C1F" CornerRadius="12" Padding="14,12">
<StackPanel>
<TextBlock Text="CPU" FontSize="10" Foreground="#52525B"
FontWeight="SemiBold" Margin="0,0,0,6"/>
<TextBlock x:Name="CpuLabel" FontSize="24" FontWeight="Bold"
Foreground="#FAFAFA" Margin="0,0,0,8"/>
<ProgressBar x:Name="CpuBar" Height="3" BorderThickness="0"
Background="#2A2A2E" Minimum="0" Maximum="100"/>
</StackPanel>
</Border>
<Border Grid.Column="2" Background="#1C1C1F" CornerRadius="12" Padding="14,12">
<StackPanel>
<TextBlock Text="RAM" FontSize="10" Foreground="#52525B"
FontWeight="SemiBold" Margin="0,0,0,6"/>
<TextBlock x:Name="RamLabel" FontSize="14" FontWeight="Bold"
Foreground="#FAFAFA" Margin="0,0,0,8" TextWrapping="Wrap"/>
<ProgressBar x:Name="RamBar" Height="3" BorderThickness="0"
Background="#2A2A2E" Minimum="0" Maximum="100"/>
</StackPanel>
</Border>
<Border Grid.Column="4" Background="#1C1C1F" CornerRadius="12" Padding="14,12">
<StackPanel>
<TextBlock Text="DISK" FontSize="10" Foreground="#52525B"
FontWeight="SemiBold" Margin="0,0,0,6"/>
<TextBlock x:Name="DiskLabel" FontSize="14" FontWeight="Bold"
Foreground="#FAFAFA" Margin="0,0,0,8" TextWrapping="Wrap"/>
<ProgressBar x:Name="DiskBar" Height="3" BorderThickness="0"
Background="#2A2A2E" Minimum="0" Maximum="100"/>
</StackPanel>
</Border>
</Grid>
<!-- Status Card -->
<Border Background="#1C1C1F" CornerRadius="12" Padding="16,14" Margin="0,0,0,10">
<StackPanel>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="&#xE72E;" FontFamily="Segoe MDL2 Assets"
FontSize="13" Foreground="#3F3F46" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="BitLocker" FontSize="12"
Foreground="#A1A1AA" VerticalAlignment="Center"/>
<TextBlock x:Name="BitlockerLabel" Grid.Column="2" FontSize="12"
FontWeight="SemiBold" VerticalAlignment="Center"/>
</Grid>
<Rectangle Height="1" Fill="#27272A" Margin="0,0,0,10"/>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="&#xEA18;" FontFamily="Segoe MDL2 Assets"
FontSize="13" Foreground="#3F3F46" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="Microsoft Defender" FontSize="12"
Foreground="#A1A1AA" VerticalAlignment="Center"/>
<TextBlock x:Name="DefenderLabel" Grid.Column="2" FontSize="12"
FontWeight="SemiBold" VerticalAlignment="Center"/>
</Grid>
<Rectangle Height="1" Fill="#27272A" Margin="0,0,0,10"/>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="&#xE895;" FontFamily="Segoe MDL2 Assets"
FontSize="13" Foreground="#3F3F46" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="Windows Updates" FontSize="12"
Foreground="#A1A1AA" VerticalAlignment="Center"/>
<TextBlock x:Name="UpdatesLabel" Grid.Column="2" FontSize="12"
FontWeight="SemiBold" VerticalAlignment="Center"/>
</Grid>
<Rectangle Height="1" Fill="#27272A" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="&#xE9D9;" FontFamily="Segoe MDL2 Assets"
FontSize="13" Foreground="#3F3F46" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="Seriennummer" FontSize="12"
Foreground="#A1A1AA" VerticalAlignment="Center"/>
<TextBlock x:Name="SerialLabel" Grid.Column="2" FontSize="11"
Foreground="#52525B" VerticalAlignment="Center"
FontFamily="Consolas"/>
</Grid>
</StackPanel>
</Border>
<!-- Installierte Software -->
<Border Background="#1C1C1F" CornerRadius="12" Padding="16,14" Margin="0,0,0,10">
<StackPanel>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="INSTALLIERTE SOFTWARE" FontSize="10" FontWeight="SemiBold"
Foreground="#52525B" VerticalAlignment="Center"/>
<TextBlock x:Name="SoftwareCountLabel" Grid.Column="1" FontSize="10"
Foreground="#3F3F46" VerticalAlignment="Center"/>
</Grid>
<Border Background="#27272A" CornerRadius="8" BorderBrush="#3F3F46"
BorderThickness="1" Padding="10,0" Margin="0,0,0,8">
<TextBox x:Name="SoftwareSearch" Background="Transparent"
Foreground="#71717A" BorderThickness="0" FontSize="12"
Height="32" VerticalContentAlignment="Center"
CaretBrush="White"
TextChanged="SoftwareSearch_TextChanged"/>
</Border>
<ListBox x:Name="SoftwareList" MaxHeight="200" Background="Transparent"
BorderThickness="0" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#A1A1AA"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Padding" Value="4,3"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" Background="{TemplateBinding Background}"
CornerRadius="4" Padding="6,3">
<TextBlock Text="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}"
FontSize="11" TextTrimming="CharacterEllipsis"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#27272A"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1D4ED820"/>
<Setter Property="Foreground" Value="#FAFAFA"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
<!-- Letzter Check-in -->
<TextBlock x:Name="LastCheckinLabel" FontSize="10" Foreground="#3F3F46"
HorizontalAlignment="Center" Margin="0,0,0,10"/>
<!-- Action Buttons -->
<Button x:Name="WebButton" Height="40" Margin="0,0,0,8"
Content="IT Nexus Web öffnen"
Click="WebButton_Click" Style="{StaticResource BtnPrimary}"
FontSize="12" FontWeight="SemiBold"/>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Height="38"
Content="Nachricht senden"
Click="MessageButton_Click"
Style="{StaticResource BtnAction}"
FontSize="11"/>
<Button Grid.Column="2" Height="38"
Content="Neustart anfordern"
Click="RebootButton_Click"
Style="{StaticResource BtnWarn}"
FontSize="11"/>
</Grid>
<!-- Admin -->
<Border Background="#1C1C1F" CornerRadius="12" Padding="14,12" Margin="0,0,0,4">
<StackPanel>
<Button x:Name="AdminLoginButton" Content="Admin-Modus"
Height="26" Click="AdminLoginButton_Click"
Style="{StaticResource BtnGhost}"/>
<StackPanel x:Name="AdminPanel" Visibility="Collapsed" Margin="0,12,0,0">
<TextBlock x:Name="AdminUserLabel" FontSize="11" Foreground="#3B82F6"
FontWeight="SemiBold" Margin="0,0,0,10"
HorizontalAlignment="Center"/>
<UniformGrid Columns="2" Rows="2">
<Button Content="Sofort Check-in" Height="34" Margin="0,0,4,4"
Click="AdminCheckin_Click" Style="{StaticResource BtnAdminAction}"/>
<Button Content="Updates prüfen" Height="34" Margin="4,0,0,4"
Click="AdminUpdates_Click" Style="{StaticResource BtnAdminAction}"/>
<Button Content="Agent-Log" Height="34" Margin="0,0,4,0"
Click="AdminLog_Click" Style="{StaticResource BtnAdminAction}"/>
<Button Content="Neu starten" Height="34" Margin="4,0,0,0"
Click="AdminRestart_Click" Style="{StaticResource BtnAdminAction}"/>
</UniformGrid>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ═══ FOOTER ═══ -->
<Border Grid.Row="2" Background="#18181B" CornerRadius="0,0,14,14"
Padding="0,10">
<TextBlock x:Name="VersionLabel" FontSize="10" Foreground="#3F3F46"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
<!-- ═══ ADMIN LOGIN MODAL OVERLAY ═══ -->
<Border x:Name="AdminOverlay" Visibility="Collapsed"
Background="#80000000" CornerRadius="14">
<Border Background="#1C1C1F" CornerRadius="12" Padding="28,24"
Width="320" VerticalAlignment="Center" HorizontalAlignment="Center">
<Border.Effect>
<DropShadowEffect Color="Black" Opacity="0.6" BlurRadius="30" ShadowDepth="0"/>
</Border.Effect>
<StackPanel>
<TextBlock Text="Admin-Anmeldung" FontSize="15" FontWeight="Bold"
Foreground="#FAFAFA" Margin="0,0,0,4"/>
<TextBlock Text="IT Nexus Zugangsdaten eingeben"
FontSize="11" Foreground="#52525B" Margin="0,0,0,20"/>
<TextBlock Text="Benutzername" FontSize="11" Foreground="#71717A" Margin="0,0,0,6"/>
<Border Background="#27272A" CornerRadius="8" BorderBrush="#3F3F46"
BorderThickness="1" Padding="12,0" Margin="0,0,0,12">
<TextBox x:Name="AdminUsernameBox" Background="Transparent"
Foreground="#FAFAFA" BorderThickness="0" FontSize="13"
Height="36" VerticalContentAlignment="Center"
CaretBrush="White"/>
</Border>
<TextBlock Text="Passwort" FontSize="11" Foreground="#71717A" Margin="0,0,0,6"/>
<Border Background="#27272A" CornerRadius="8" BorderBrush="#3F3F46"
BorderThickness="1" Padding="12,0" Margin="0,0,0,6">
<PasswordBox x:Name="AdminPasswordBox" Background="Transparent"
Foreground="#FAFAFA" BorderThickness="0" FontSize="13"
Height="36" VerticalContentAlignment="Center"
KeyDown="AdminPasswordBox_KeyDown"/>
</Border>
<TextBlock x:Name="AdminErrorLabel" FontSize="11" Foreground="#EF4444"
Margin="0,4,0,0" Visibility="Collapsed"/>
<Grid Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Abbrechen" Height="38"
Click="AdminCancelModal_Click"
Style="{StaticResource BtnAction}" FontSize="12"/>
<Button x:Name="AdminLoginBtn" Grid.Column="2" Content="Anmelden" Height="38"
Click="AdminLoginModal_Click"
Style="{StaticResource BtnPrimary}" FontSize="12" FontWeight="SemiBold"/>
</Grid>
</StackPanel>
</Border>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,503 @@
using ITNexusAgent.Models;
using ITNexusAgent.Services;
using Newtonsoft.Json;
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;
using WinForms = System.Windows.Forms;
using DrawingIcon = System.Drawing.Icon;
using DrawingSystemIcons = System.Drawing.SystemIcons;
using MessageBox = System.Windows.MessageBox;
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
using MediaColor = System.Windows.Media.Color;
using ProgressBar = System.Windows.Controls.ProgressBar;
namespace ITNexusAgent.UI;
public partial class DashboardWindow : Window
{
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
private const string LogPath = @"C:\ProgramData\IT Nexus Agent\agent.log";
private readonly DispatcherTimer _timer;
private AgentConfig? _config;
private string? _adminToken;
private WinForms.NotifyIcon? _trayIcon;
private List<string> _allSoftware = [];
private bool _softwarePlaceholderActive = true;
public DashboardWindow()
{
InitializeComponent();
try { _config = AgentConfig.Load(@"C:\ProgramData\IT Nexus Agent\config.json"); }
catch { }
InitTrayIcon();
InitSoftwareSearch();
VersionLabel.Text = $"IT Nexus Agent v2.0.0 · Cereda Systems GmbH";
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
_timer.Tick += (_, _) => RefreshStatus();
_timer.Start();
RefreshStatus();
}
private void InitSoftwareSearch()
{
SoftwareSearch.Text = "Software suchen...";
SoftwareSearch.Foreground = new SolidColorBrush(MediaColor.FromRgb(82, 82, 91));
SoftwareSearch.GotFocus += (_, _) =>
{
if (_softwarePlaceholderActive)
{
_softwarePlaceholderActive = false;
SoftwareSearch.Text = "";
SoftwareSearch.Foreground = new SolidColorBrush(MediaColor.FromRgb(250, 250, 250));
}
};
SoftwareSearch.LostFocus += (_, _) =>
{
if (string.IsNullOrWhiteSpace(SoftwareSearch.Text))
{
_softwarePlaceholderActive = true;
SoftwareSearch.Text = "Software suchen...";
SoftwareSearch.Foreground = new SolidColorBrush(MediaColor.FromRgb(82, 82, 91));
ApplySoftwareFilter("");
}
};
}
private void RefreshStatus()
{
try
{
if (!File.Exists(StatusPath))
{
SetOffline("Noch kein Check-in");
return;
}
var json = File.ReadAllText(StatusPath);
var status = JsonConvert.DeserializeObject<StatusCache>(json);
if (status == null) return;
HostnameLabel.Text = $"{status.Hostname} · {status.LastUser}";
var ago = DateTime.Now - status.LastCheckin;
var agoText = ago.TotalSeconds < 60
? $"vor {(int)ago.TotalSeconds} Sekunden"
: ago.TotalMinutes < 60
? $"vor {(int)ago.TotalMinutes} Minuten"
: $"vor {(int)ago.TotalHours} Stunden";
if (status.Online && ago.TotalMinutes < 3)
{
StatusDot.Fill = new SolidColorBrush(MediaColor.FromRgb(34, 197, 94));
StatusLabel.Text = "Online";
}
else
{
StatusDot.Fill = new SolidColorBrush(MediaColor.FromRgb(239, 68, 68));
StatusLabel.Text = "Offline";
}
// Metrics
var cpu = status.CpuUsage ?? 0;
CpuLabel.Text = $"{cpu:0}%";
CpuBar.Value = cpu;
SetBarColor(CpuBar, cpu);
if (status.RamTotal > 0)
{
var ramPct = (status.RamUsed ?? 0) / status.RamTotal.Value * 100;
RamLabel.Text = $"{status.RamUsed:0.0}/{status.RamTotal:0} GB";
RamBar.Value = ramPct;
SetBarColor(RamBar, ramPct);
}
if (status.DiskTotal > 0)
{
var diskUsed = status.DiskTotal.Value - (status.DiskFree ?? 0);
var diskPct = diskUsed / status.DiskTotal.Value * 100;
DiskLabel.Text = $"{diskUsed:0}/{status.DiskTotal:0} GB";
DiskBar.Value = diskPct;
SetBarColor(DiskBar, diskPct);
}
// Security
BitlockerLabel.Text = status.BitlockerStatus switch
{
"encrypted" => "✓ Verschlüsselt",
"off" => "✗ Nicht aktiv",
_ => "? Unbekannt"
};
BitlockerLabel.Foreground = status.BitlockerStatus == "encrypted"
? new SolidColorBrush(MediaColor.FromRgb(22, 163, 74))
: new SolidColorBrush(MediaColor.FromRgb(239, 68, 68));
DefenderLabel.Text = status.DefenderEnabled
? status.DefenderSignaturesAge <= 3
? "✓ Aktiv · Signaturen aktuell"
: $"⚠ Aktiv · Signaturen {status.DefenderSignaturesAge}d alt"
: "✗ Inaktiv";
DefenderLabel.Foreground = status.DefenderEnabled
? new SolidColorBrush(MediaColor.FromRgb(22, 163, 74))
: new SolidColorBrush(MediaColor.FromRgb(239, 68, 68));
UpdatesLabel.Text = status.PendingUpdates == 0 ? "✓ Aktuell" : $"⚠ {status.PendingUpdates} ausstehend";
UpdatesLabel.Foreground = status.PendingUpdates == 0
? new SolidColorBrush(MediaColor.FromRgb(22, 163, 74))
: new SolidColorBrush(MediaColor.FromRgb(245, 158, 11));
SerialLabel.Text = string.IsNullOrEmpty(status.HardwareSerial) ? "—" : status.HardwareSerial;
LastCheckinLabel.Text = $"Letzter Check-in: {agoText}";
// Software-Liste nur aktualisieren wenn sich die Anzahl geändert hat
if (status.InstalledSoftware.Count != _allSoftware.Count ||
(status.InstalledSoftware.Count > 0 && _allSoftware.Count == 0))
{
_allSoftware = status.InstalledSoftware;
var filterText = _softwarePlaceholderActive ? "" : SoftwareSearch.Text;
ApplySoftwareFilter(filterText);
}
}
catch { SetOffline("Fehler beim Laden"); }
}
private void ApplySoftwareFilter(string filter)
{
var filtered = string.IsNullOrWhiteSpace(filter)
? _allSoftware
: _allSoftware.Where(s => s.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList();
SoftwareList.ItemsSource = filtered;
SoftwareCountLabel.Text = _allSoftware.Count > 0
? $"{filtered.Count} / {_allSoftware.Count}"
: "";
}
private void SoftwareSearch_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
if (_softwarePlaceholderActive) return;
ApplySoftwareFilter(SoftwareSearch.Text);
}
private void SetOffline(string reason)
{
StatusDot.Fill = new SolidColorBrush(MediaColor.FromRgb(156, 163, 175));
StatusLabel.Text = reason;
}
private static void SetBarColor(System.Windows.Controls.ProgressBar bar, double pct)
{
var color = pct > 90 ? MediaColor.FromRgb(239, 68, 68)
: pct > 70 ? MediaColor.FromRgb(245, 158, 11)
: MediaColor.FromRgb(59, 130, 246);
bar.Foreground = new SolidColorBrush(color);
}
private void WebButton_Click(object sender, RoutedEventArgs e)
{
var url = _config?.ServerUrl ?? "https://it-nexus.cereda-systems.de";
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
private async void MessageButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new MessageDialog();
if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.Message)) return;
try
{
if (_config != null)
{
var api = new ApiService(_config.ServerUrl, _config.AgentKey);
await api.SendMessageAsync(Environment.MachineName, dlg.Message);
MessageBox.Show("Nachricht wurde gesendet.", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch
{
MessageBox.Show("Fehler beim Senden.", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private async void RebootButton_Click(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show(
"Möchtest du einen Neustart bei der IT anfordern?\nDein Rechner wird nicht sofort neu gestartet.",
"Neustart anfordern", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
try
{
if (_config != null)
{
var api = new ApiService(_config.ServerUrl, _config.AgentKey);
await api.SendRebootRequestAsync(Environment.MachineName);
MessageBox.Show("Neustart wurde angefordert. Die IT wird sich darum kümmern.",
"IT Nexus", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch { }
}
private void AdminLoginButton_Click(object sender, RoutedEventArgs e)
{
if (_adminToken != null)
{
_adminToken = null;
AdminPanel.Visibility = Visibility.Collapsed;
AdminLoginButton.Content = "Admin-Modus";
return;
}
AdminUsernameBox.Text = "";
AdminPasswordBox.Password = "";
AdminErrorLabel.Visibility = Visibility.Collapsed;
AdminOverlay.Visibility = Visibility.Visible;
AdminUsernameBox.Focus();
}
private void AdminCancelModal_Click(object sender, RoutedEventArgs e)
=> AdminOverlay.Visibility = Visibility.Collapsed;
private async void AdminLoginModal_Click(object sender, RoutedEventArgs e)
{
var username = AdminUsernameBox.Text.Trim();
var password = AdminPasswordBox.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return;
AdminLoginBtn.IsEnabled = false;
AdminErrorLabel.Visibility = Visibility.Collapsed;
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var body = Newtonsoft.Json.JsonConvert.SerializeObject(new { username, password });
var resp = await http.PostAsync($"{_config?.ServerUrl?.TrimEnd('/')}/api/auth/login",
new StringContent(body, Encoding.UTF8, "application/json"));
var json = await resp.Content.ReadAsStringAsync();
var result = Newtonsoft.Json.JsonConvert.DeserializeAnonymousType(json, new { token = "" });
if (resp.IsSuccessStatusCode && !string.IsNullOrEmpty(result?.token))
{
_adminToken = result.token;
AdminOverlay.Visibility = Visibility.Collapsed;
if (_pendingExit)
{
_pendingExit = false;
DoExit();
return;
}
AdminPanel.Visibility = Visibility.Visible;
AdminUserLabel.Text = $"Angemeldet als {username}";
AdminLoginButton.Content = "Admin-Modus beenden";
}
else
{
AdminErrorLabel.Text = "Ungültige Anmeldedaten";
AdminErrorLabel.Visibility = Visibility.Visible;
}
}
catch
{
AdminErrorLabel.Text = "Verbindung fehlgeschlagen";
AdminErrorLabel.Visibility = Visibility.Visible;
}
finally { AdminLoginBtn.IsEnabled = true; }
}
private void AdminPasswordBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Enter)
AdminLoginModal_Click(sender, new RoutedEventArgs());
}
private void AdminCheckin_Click(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo("net.exe", "stop \"IT Nexus Agent\"")
{ CreateNoWindow = true, UseShellExecute = false })?.WaitForExit();
Process.Start(new ProcessStartInfo("net.exe", "start \"IT Nexus Agent\"")
{ CreateNoWindow = true, UseShellExecute = false });
MessageBox.Show("Agent wird neu gestartet — Check-in in ~5 Sekunden.",
"IT Nexus", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Fehler: {ex.Message}", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void AdminUpdates_Click(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo("UsoClient.exe", "StartScan") { CreateNoWindow = true });
MessageBox.Show("Update-Scan gestartet.", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void AdminLog_Click(object sender, RoutedEventArgs e)
{
var dlg = new LogWindow();
dlg.Show();
}
private void AdminRestart_Click(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo("net.exe", "stop \"IT Nexus Agent\"")
{ CreateNoWindow = true, UseShellExecute = false })?.WaitForExit();
Process.Start(new ProcessStartInfo("net.exe", "start \"IT Nexus Agent\"")
{ CreateNoWindow = true, UseShellExecute = false });
MessageBox.Show("Agent-Service wird neu gestartet.", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Fehler: {ex.Message}", "IT Nexus", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void InitTrayIcon()
{
try
{
var iconPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "icon.ico");
var icon = File.Exists(iconPath)
? new DrawingIcon(iconPath)
: DrawingSystemIcons.Application;
_trayIcon = new WinForms.NotifyIcon
{
Icon = icon,
Text = "IT Nexus Agent",
Visible = true
};
var menu = new WinForms.ContextMenuStrip();
menu.Items.Add("Dashboard öffnen", null, (s, e) => ShowDashboard());
menu.Items.Add("-");
menu.Items.Add("IT Nexus Web", null, (s, e) =>
{
var url = _config?.ServerUrl ?? "https://it-nexus.cereda-systems.de";
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
});
menu.Items.Add("-");
menu.Items.Add("Beenden", null, (s, e) => ExitWithAuth());
_trayIcon.ContextMenuStrip = menu;
_trayIcon.DoubleClick += (s, e) => ShowDashboard();
}
catch { }
}
private void ShowDashboard()
{
Show();
WindowState = WindowState.Normal;
Activate();
Focus();
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
Hide();
}
private void ExitWithAuth()
{
Dispatcher.Invoke(() =>
{
if (_adminToken != null)
{
DoExit();
return;
}
AdminUsernameBox.Text = "";
AdminPasswordBox.Password = "";
AdminErrorLabel.Visibility = Visibility.Collapsed;
AdminOverlay.Visibility = Visibility.Visible;
ShowDashboard();
_pendingExit = true;
AdminUsernameBox.Focus();
});
}
private bool _pendingExit = false;
private void DoExit()
{
_trayIcon!.Visible = false;
System.Windows.Application.Current.Shutdown();
}
private void TitleBar_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
DragMove();
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
private bool _isMaximized = false;
private double _restoreLeft, _restoreTop, _restoreWidth, _restoreHeight;
private void Minimize_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState.Minimized;
private void Maximize_Click(object sender, RoutedEventArgs e)
{
if (_isMaximized)
RestoreWindow();
else
MaximizeWindow();
}
private void MaximizeWindow()
{
_restoreLeft = Left; _restoreTop = Top;
_restoreWidth = Width; _restoreHeight = Height;
var wa = SystemParameters.WorkArea;
Left = wa.Left; Top = wa.Top;
Width = wa.Width; Height = wa.Height;
_isMaximized = true;
OuterBorder.CornerRadius = new CornerRadius(0);
OuterBorder.Margin = new Thickness(0);
OuterBorder.Effect = null;
TitleBarBorder.CornerRadius = new CornerRadius(0);
}
private void RestoreWindow()
{
Left = _restoreLeft; Top = _restoreTop;
Width = _restoreWidth; Height = _restoreHeight;
_isMaximized = false;
OuterBorder.CornerRadius = new CornerRadius(14);
OuterBorder.Margin = new Thickness(16);
OuterBorder.Effect = new System.Windows.Media.Effects.DropShadowEffect
{
Color = System.Windows.Media.Colors.Black,
Opacity = 0.4, BlurRadius = 20, ShadowDepth = 0
};
TitleBarBorder.CornerRadius = new CornerRadius(14, 14, 0, 0);
}
private void Window_StateChanged(object sender, EventArgs e) { }
protected override void OnClosed(EventArgs e)
{
_timer.Stop();
base.OnClosed(e);
}
}

View File

@@ -0,0 +1,31 @@
<Window x:Class="ITNexusAgent.UI.LogWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Agent Log"
Width="700" Height="500"
WindowStartupLocation="CenterScreen"
Background="#1E1E2E" FontFamily="Consolas">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<TextBox x:Name="LogText" Grid.Row="0"
Background="#1E1E2E" Foreground="#CDD6F4"
FontSize="11" IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
BorderThickness="0" Padding="12"
TextWrapping="NoWrap"/>
<Border Grid.Row="1" Background="#181825" Padding="12,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="Aktualisieren" Height="26" Padding="10,0"
Click="Refresh_Click" Background="#313244" Foreground="#CDD6F4"
BorderThickness="0" FontFamily="Segoe UI" FontSize="11" Cursor="Hand"/>
<Button Content="Log leeren" Height="26" Padding="10,0" Margin="8,0,0,0"
Click="Clear_Click" Background="#313244" Foreground="#F38BA8"
BorderThickness="0" FontFamily="Segoe UI" FontSize="11" Cursor="Hand"/>
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,40 @@
using System.Windows;
using MessageBox = System.Windows.MessageBox;
namespace ITNexusAgent.UI;
public partial class LogWindow : Window
{
private const string LogPath = @"C:\ProgramData\IT Nexus Agent\agent.log";
public LogWindow()
{
InitializeComponent();
LoadLog();
}
private void LoadLog()
{
try
{
if (File.Exists(LogPath))
{
var lines = File.ReadAllLines(LogPath);
LogText.Text = string.Join(Environment.NewLine, lines.TakeLast(500));
LogText.ScrollToEnd();
}
else LogText.Text = "Log-Datei nicht gefunden.";
}
catch (Exception ex) { LogText.Text = $"Fehler: {ex.Message}"; }
}
private void Refresh_Click(object sender, RoutedEventArgs e) => LoadLog();
private void Clear_Click(object sender, RoutedEventArgs e)
{
var r = MessageBox.Show("Log wirklich löschen?", "IT Nexus",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (r != MessageBoxResult.Yes) return;
try { File.WriteAllText(LogPath, ""); LoadLog(); } catch { }
}
}

View File

@@ -0,0 +1,22 @@
<Window x:Class="ITNexusAgent.UI.MessageDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Nachricht an IT senden"
Width="400" Height="220"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize" Background="White" FontFamily="Segoe UI">
<StackPanel Margin="20">
<TextBlock Text="Nachricht an das IT-Team" FontSize="13" FontWeight="SemiBold"
Foreground="#1F2937" Margin="0,0,0,12"/>
<TextBox x:Name="MessageBox" Height="80" TextWrapping="Wrap" AcceptsReturn="True"
Padding="8" FontSize="12" BorderBrush="#D1D5DB"
VerticalScrollBarVisibility="Auto"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
<Button Content="Abbrechen" Width="80" Height="32" Margin="0,0,8,0"
Click="Cancel_Click" Background="#F9FAFB" BorderBrush="#D1D5DB" FontSize="12"/>
<Button Content="Senden" Width="80" Height="32"
Click="Send_Click" Background="#0078D4" Foreground="White"
BorderThickness="0" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
</StackPanel>
</Window>

View File

@@ -0,0 +1,18 @@
using System.Windows;
namespace ITNexusAgent.UI;
public partial class MessageDialog : Window
{
public string Message { get; private set; } = "";
public MessageDialog() => InitializeComponent();
private void Send_Click(object sender, RoutedEventArgs e)
{
Message = MessageBox.Text.Trim();
DialogResult = true;
}
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
}

View File

@@ -0,0 +1,108 @@
<Window x:Class="ITNexusAgent.UI.NotificationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IT Nexus Mitteilung"
Width="500" Height="Auto"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
FontFamily="Segoe UI">
<Window.Resources>
<Style x:Key="ConfirmBtn" TargetType="Button">
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Height" Value="46"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bg" CornerRadius="8"
Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bg" Property="Opacity" Value="0.85"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bg" Property="Opacity" Value="0.7"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<!-- Äußerer Rahmen mit Schatten + farbigem Top-Border -->
<Border CornerRadius="12" Background="#1A1D2E"
BorderBrush="#2D3250" BorderThickness="1">
<Border.Effect>
<DropShadowEffect Color="Black" BlurRadius="32" ShadowDepth="8" Opacity="0.7"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="4"/> <!-- Farbige Top-Linie -->
<RowDefinition Height="Auto"/> <!-- Header: Icon + Typ + Titel -->
<RowDefinition Height="Auto"/> <!-- Nachricht -->
<RowDefinition Height="Auto"/> <!-- Meta -->
<RowDefinition Height="Auto"/> <!-- Button -->
</Grid.RowDefinitions>
<!-- Farbige Linie oben (wie Web: border-top) -->
<Border Grid.Row="0" x:Name="TopAccent" CornerRadius="12,12,0,0"/>
<!-- Header -->
<StackPanel Grid.Row="1" Orientation="Horizontal"
Margin="24,20,24,16" VerticalAlignment="Center">
<!-- Icon-Box -->
<Border x:Name="IconBox" Width="44" Height="44" CornerRadius="10"
VerticalAlignment="Top" Margin="0,0,14,0">
<TextBlock x:Name="IconText" FontSize="22"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- Typ + Titel -->
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TypeLabel" FontSize="10" FontWeight="Bold"
TextOptions.TextFormattingMode="Display"/>
<TextBlock x:Name="TitleText" FontSize="17" FontWeight="Bold"
Foreground="White" TextWrapping="Wrap" MaxWidth="370"
Margin="0,3,0,0" LineHeight="22"/>
</StackPanel>
</StackPanel>
<!-- Nachricht -->
<Border Grid.Row="2" Margin="24,0,24,16"
Background="#252840" CornerRadius="8"
BorderBrush="#3D4270" BorderThickness="1" Padding="14,12">
<TextBlock x:Name="MessageText" FontSize="13" Foreground="#B0B8D1"
TextWrapping="Wrap" LineHeight="20"/>
</Border>
<!-- Meta -->
<StackPanel Grid.Row="3" Orientation="Horizontal"
Margin="24,0,24,20" Opacity="0.55">
<TextBlock x:Name="MetaText" FontSize="11" Foreground="#8B9BBF"/>
</StackPanel>
<!-- Button + Hinweis -->
<StackPanel Grid.Row="4" Margin="24,0,24,24">
<Button x:Name="ConfirmButton" Style="{StaticResource ConfirmBtn}"
Click="ConfirmButton_Click">
<TextBlock Text="✓ Gelesen und bestätigt" FontSize="13" FontWeight="Bold"/>
</Button>
<TextBlock Text="Du musst diese Nachricht bestätigen um fortzufahren."
FontSize="10" Foreground="#555E7A"
HorizontalAlignment="Center" Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,59 @@
using ITNexusAgent.Models;
using ITNexusAgent.Services;
using System.Windows;
using System.Windows.Media;
namespace ITNexusAgent.UI;
public partial class NotificationWindow : Window
{
private readonly Announcement _ann;
private readonly ApiService? _api;
public NotificationWindow(Announcement ann)
{
InitializeComponent();
_ann = ann;
var (hex, label, icon) = ann.Type switch
{
"warning" => ("#DC3545", "WICHTIGE WARNUNG", "⚠️"),
"maintenance" => ("#E67E22", "WARTUNGSANKÜNDIGUNG", "🔧"),
_ => ("#5865F2", "INFORMATION", ""),
};
var color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(hex);
var brush = new SolidColorBrush(color);
var iconBg = new SolidColorBrush(System.Windows.Media.Color.FromArgb(40, color.R, color.G, color.B));
TopAccent.Background = brush;
IconBox.Background = iconBg;
IconBox.BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(80, color.R, color.G, color.B));
IconBox.BorderThickness = new Thickness(1);
IconText.Text = icon;
TypeLabel.Text = label;
TypeLabel.Foreground = brush;
TitleText.Text = ann.Title;
MessageText.Text = ann.Message;
ConfirmButton.Background = brush;
MetaText.Text = $"IT Nexus Mitteilung";
try
{
var cfg = AgentConfig.Load(@"C:\ProgramData\IT Nexus Agent\config.json");
_api = new ApiService(cfg.ServerUrl, cfg.AgentKey);
}
catch { }
}
private async void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (_api != null)
await _api.AckAnnouncementAsync(_ann.Id, Environment.MachineName);
}
catch { }
Close();
}
}

22
agent-cs/app.manifest Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.0.0.0" name="IT-Nexus-Agent.exe" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,4 @@
{
"server_url": "https://it-nexus.cereda-systems.de",
"agent_key": "HIER-DEN-AGENT-KEY-EINTRAGEN"
}

View File

@@ -0,0 +1,4 @@
if (Test-Path "C:\Program Files\IT Nexus Agent\IT-Nexus-Agent.exe") {
exit 0
}
exit 1

BIN
agent-cs/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,629 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>IT Nexus — Live Netzwerk-Topologie</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #04040A; font-family: 'Segoe UI', system-ui, sans-serif; color: #E4E4E7; }
/* ── TOPBAR (mit Buttons) ── */
#topbar {
position: fixed; top: 0; left: 0; right: 0; height: 52px;
background: rgba(6,6,14,0.99); border-bottom: 1px solid #0F0F1A;
display: flex; align-items: center; gap: 10px; padding: 0 16px;
z-index: 100;
}
#topbar-title { font-size: 13px; font-weight: 700; color: #FAFAFA; margin-right: 16px; white-space: nowrap; }
#topbar-title em { color: #009B9A; font-style: normal; }
.sim-sep { width: 1px; height: 22px; background: #1A1A24; margin: 0 6px; }
.sim-lbl { font-size: 9px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: #27272A; white-space: nowrap; }
.tb {
padding: 6px 13px; border-radius: 8px;
border: 1px solid #1A1A24;
background: rgba(14,14,22,0.9);
color: #52525B; font-size: 11px; font-weight: 600;
cursor: pointer; font-family: inherit; white-space: nowrap;
transition: all 0.2s;
}
.tb:hover { border-color: #3F3F46; color: #E4E4E7; background: rgba(30,30,44,0.9); }
.tb:active { transform: scale(0.97); }
.tb.active-flow { border-color: var(--tc); color: var(--tc); box-shadow: 0 0 8px var(--tc-g); }
/* ── LOG PANEL (links, volle Höhe) ── */
#logpanel {
position: fixed; top: 52px; left: 0; bottom: 0; width: 270px;
background: rgba(4,4,12,0.99); border-right: 1px solid #0D0D1A;
padding: 18px 16px; z-index: 80; overflow: hidden;
display: flex; flex-direction: column;
}
#log-lbl {
font-size: 8.5px; font-weight: 800; letter-spacing: 0.14em;
color: #1A1A28; text-transform: uppercase; margin-bottom: 16px;
flex-shrink: 0;
}
#log-body { display: flex; flex-direction: column; gap: 0; flex: 1; overflow: hidden; }
.ll {
display: flex; flex-direction: column; gap: 3px;
padding: 10px 12px 10px 14px;
border-left: 2px solid var(--lc, #1E1E2E);
margin-bottom: 8px;
background: rgba(255,255,255,0.018);
border-radius: 0 8px 8px 0;
animation: fadeIn .3s ease;
}
@keyframes fadeIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
.ll-t {
font-family: 'Consolas', monospace;
font-size: 10px; color: #2A2A3C;
}
.ll-step { font-size: 10px; font-weight: 700; color: #3A3A50; margin-bottom: 2px; }
.ll-m { font-size: 13px; font-weight: 600; line-height: 1.4; }
/* ── NODES ── */
.node {
position: fixed; z-index: 20;
background: rgba(10,10,18,0.97);
border: 1px solid #14141E;
border-radius: 11px;
padding: 10px 13px;
width: 158px;
backdrop-filter: blur(12px);
transition: box-shadow 0.25s, border-color 0.25s, transform 0.15s;
cursor: default; user-select: none;
}
.node:hover { border-color: var(--nc, #27272A); transform: translateY(-1px); z-index: 30; }
.node.lit {
border-color: var(--nc) !important;
box-shadow: 0 0 0 1px var(--nc), 0 0 20px var(--ng), inset 0 0 14px var(--ni);
}
.nh { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.ni { font-size: 15px; line-height: 1; flex-shrink: 0; }
.nn { font-size: 11.5px; font-weight: 700; color: #F4F4F5; }
.nb {
margin-left: auto; font-size: 8.5px; font-weight: 700;
padding: 2px 6px; border-radius: 4px; white-space: nowrap;
background: rgba(34,197,94,.14); color: #4ADE80;
}
.nb.w { background: rgba(251,191,36,.14); color: #FCD34D; }
.nb.g { background: rgba(82,82,91,.16); color: #52525B; }
.nb.b { background: rgba(59,130,246,.14); color: #93C5FD; }
.ns { font-size: 9.5px; color: #2E2E42; line-height: 1.45; }
/* ── ZONE LABELS ── */
.zl {
position: fixed; z-index: 25;
font-size: 8.5px; font-weight: 800; letter-spacing: 0.14em;
text-transform: uppercase; color: #14141E; pointer-events: none;
}
/* ── LEGEND ── */
#legend {
position: fixed; right: 14px; top: 62px; z-index: 80;
background: rgba(6,6,14,0.97); border: 1px solid #0E0E18;
border-radius: 11px; padding: 12px 14px; width: 178px;
backdrop-filter: blur(12px);
}
#legend h3 { font-size: 8.5px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #22222E; margin-bottom: 9px; }
.li { display: flex; align-items: center; gap: 9px; font-size: 10px; color: #52525B; margin-bottom: 5px; }
.ld { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
/* ── BG + SVG ── */
#bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
#sv { position: fixed; inset: 0; z-index: 10; pointer-events: none; overflow: visible; }
</style>
</head>
<body>
<canvas id="bg"></canvas>
<svg id="sv" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="3.5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow2" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="7" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
</svg>
<!-- ════ TOPBAR ════ -->
<div id="topbar">
<span id="topbar-title">IT <em>Nexus</em> — Architektur</span>
<div class="sim-sep"></div>
<span class="sim-lbl">Simulation:</span>
<button class="tb" style="--tc:#3B82F6;--tc-g:rgba(59,130,246,.3)" onclick="doCheckin()">🔄 Check-in</button>
<button class="tb" style="--tc:#F59E0B;--tc-g:rgba(245,158,11,.3)" onclick="doAnnouncement()">📢 Ankündigung</button>
<button class="tb" style="--tc:#EF4444;--tc-g:rgba(239,68,68,.3)" onclick="doPatch()">🔧 Patch-Befehl</button>
<button class="tb" style="--tc:#2DD4BF;--tc-g:rgba(45,212,191,.3)" onclick="doUpdate()">⬇️ Agent-Update</button>
<button class="tb" style="--tc:#0EA5E9;--tc-g:rgba(14,165,233,.3)" onclick="doIntune()">📦 Intune-Deploy</button>
</div>
<!-- ════ NODES ════ -->
<div class="zl" id="zl-pc">Windows-Endgerät</div>
<div class="node" id="n-svc" style="--nc:#3B82F6;--ng:rgba(59,130,246,.25);--ni:rgba(59,130,246,.04)">
<div class="nh"><span class="ni">⚙️</span><span class="nn">Agent Service</span><span class="nb">Running</span></div>
<div class="ns">Windows Service · SYSTEM<br>Check-in · Commands · Auto-Update</div>
</div>
<div class="node" id="n-dash" style="--nc:#6366F1;--ng:rgba(99,102,241,.25);--ni:rgba(99,102,241,.04)">
<div class="nh"><span class="ni">📊</span><span class="nn">Dashboard</span><span class="nb b">Tray</span></div>
<div class="ns">WPF · Dark-Theme · Autostart<br>Liest status.json alle 5 Sek</div>
</div>
<div class="node" id="n-nfy" style="--nc:#F59E0B;--ng:rgba(245,158,11,.25);--ni:rgba(245,158,11,.04)">
<div class="nh"><span class="ni">🔔</span><span class="nn">Notification</span><span class="nb g">On Demand</span></div>
<div class="ns">WPF Popup · schtasks.exe<br>Läuft als eingeloggter User</div>
</div>
<div class="zl" id="zl-srv">IT Nexus Server · 192.168.0.194</div>
<div class="node" id="n-ngx" style="--nc:#22C55E;--ng:rgba(34,197,94,.25);--ni:rgba(34,197,94,.04)">
<div class="nh"><span class="ni">🌐</span><span class="nn">nginx</span><span class="nb">Port 443</span></div>
<div class="ns">Reverse Proxy · TLS<br>/api/* → Backend · / → Frontend</div>
</div>
<div class="node" id="n-be" style="--nc:#10B981;--ng:rgba(16,185,129,.25);--ni:rgba(16,185,129,.04)">
<div class="nh"><span class="ni"></span><span class="nn">Node.js Backend</span><span class="nb">Port 5000</span></div>
<div class="ns">Express · JWT · Agent-Key Auth<br>Cron Jobs · KI-Integration</div>
</div>
<div class="node" id="n-db" style="--nc:#6B7280;--ng:rgba(107,114,128,.22);--ni:rgba(107,114,128,.04)">
<div class="nh"><span class="ni">🗄️</span><span class="nn">SQLite DB</span><span class="nb g">Volume</span></div>
<div class="ns">monitoring_agents · tickets<br>patch_commands · announcements</div>
</div>
<div class="node" id="n-fe" style="--nc:#3B82F6;--ng:rgba(59,130,246,.25);--ni:rgba(59,130,246,.04)">
<div class="nh"><span class="ni">⚛️</span><span class="nn">React Frontend</span><span class="nb b">Port 3000</span></div>
<div class="ns">Monitoring · Patch · Helpdesk<br>KI-Assistent · Dokumentation</div>
</div>
<div class="zl" id="zl-ext">Externe Dienste</div>
<div class="node" id="n-grp" style="--nc:#818CF8;--ng:rgba(129,140,248,.25);--ni:rgba(129,140,248,.04)">
<div class="nh"><span class="ni">☁️</span><span class="nn">Microsoft Graph</span><span class="nb w">Azure AD</span></div>
<div class="ns">Teams · MDO Alerts · Mail<br>SecurityAlert.Read.All</div>
</div>
<div class="node" id="n-ai" style="--nc:#2DD4BF;--ng:rgba(45,212,191,.25);--ni:rgba(45,212,191,.04)">
<div class="nh"><span class="ni">🤖</span><span class="nn">Anthropic Claude</span><span class="nb w">claude-sonnet-4-6</span></div>
<div class="ns">KI-Assistent · Wissensdatenbank<br>Ticket-Analyse · Automatisierung</div>
</div>
<div class="node" id="n-pmx" style="--nc:#F97316;--ng:rgba(249,115,22,.25);--ni:rgba(249,115,22,.04)">
<div class="nh"><span class="ni">🖥️</span><span class="nn">Proxmox API</span><span class="nb w">hve-01</span></div>
<div class="ns">VM/CT Status · Ressourcen<br>192.168.0.184:8006</div>
</div>
<div class="zl" id="zl-adm">IT-Administrator</div>
<div class="node" id="n-web" style="--nc:#E4E4E7;--ng:rgba(228,228,231,.15);--ni:rgba(228,228,231,.03)">
<div class="nh"><span class="ni">🧑‍💼</span><span class="nn">Web Browser</span><span class="nb">HTTPS</span></div>
<div class="ns">it-nexus.cereda-systems.de<br>Monitoring · Patch · Helpdesk</div>
</div>
<div class="node" id="n-itune" style="--nc:#0EA5E9;--ng:rgba(14,165,233,.25);--ni:rgba(14,165,233,.04)">
<div class="nh"><span class="ni">📦</span><span class="nn">Intune / MDM</span><span class="nb g">Deploy</span></div>
<div class="ns">setup.exe · GUID-Erkennung<br>Rollout auf Endgeräte</div>
</div>
<!-- ════ LEGEND ════ -->
<div id="legend">
<h3>Datenflüsse</h3>
<div class="li"><div class="ld" style="background:#3B82F6;box-shadow:0 0 5px #3B82F6"></div>Check-in (60s)</div>
<div class="li"><div class="ld" style="background:#22C55E;box-shadow:0 0 5px #22C55E"></div>Response / Commands</div>
<div class="li"><div class="ld" style="background:#6B7280;box-shadow:0 0 4px #6B7280"></div>SQLite DB-Operationen</div>
<div class="li"><div class="ld" style="background:#6366F1;box-shadow:0 0 5px #6366F1"></div>Dashboard Refresh</div>
<div class="li"><div class="ld" style="background:#818CF8;box-shadow:0 0 5px #818CF8"></div>Microsoft Graph API</div>
<div class="li"><div class="ld" style="background:#2DD4BF;box-shadow:0 0 5px #2DD4BF"></div>Anthropic Claude AI</div>
<div class="li"><div class="ld" style="background:#F97316;box-shadow:0 0 5px #F97316"></div>Proxmox API</div>
<div class="li"><div class="ld" style="background:#F4F4F5;box-shadow:0 0 4px #F4F4F5"></div>Admin Web-Zugriff</div>
<div class="li"><div class="ld" style="background:#F59E0B;box-shadow:0 0 5px #F59E0B"></div>Ankündigung</div>
<div class="li"><div class="ld" style="background:#EF4444;box-shadow:0 0 5px #EF4444"></div>Patch-Command / Update</div>
</div>
<!-- ════ LOG PANEL LINKS ════ -->
<div id="logpanel">
<div id="log-lbl">Was passiert gerade</div>
<div id="log-body">
<div class="ll" style="--lc:#1E1E2E">
<div class="ll-t"></div>
<div class="ll-m" style="color:#27273A">Oben einen Button drücken<br>um eine Simulation zu starten</div>
</div>
</div>
</div>
<script>
// ════════════════════════════════════════════════════════
// BACKGROUND GRID
// ════════════════════════════════════════════════════════
const bgc = document.getElementById('bg');
const bgx = bgc.getContext('2d');
function drawGrid() {
bgc.width = innerWidth; bgc.height = innerHeight;
bgx.fillStyle = '#04040A';
bgx.fillRect(0, 0, bgc.width, bgc.height);
// Subtle dot grid
bgx.fillStyle = 'rgba(255,255,255,0.028)';
for (let x = 0; x < bgc.width; x += 52)
for (let y = 0; y < bgc.height; y += 52) {
bgx.beginPath(); bgx.arc(x, y, 0.8, 0, Math.PI*2); bgx.fill();
}
// Zone backgrounds
const zones = [
{ x: LP+4, w: C(0)+NW/2-LP-4, color: 'rgba(59,130,246,0.018)' },
{ x: C(1)-NW/2-16, w: NW+32, color: 'rgba(34,197,94,0.015)' },
{ x: C(2)-NW/2-16, w: NW+32, color: 'rgba(129,140,248,0.015)'},
{ x: C(3)-NW/2-16, w: innerWidth-C(3)+NW/2, color: 'rgba(228,228,231,0.012)'},
];
const top = TB, bot = bgc.height - LH;
zones.forEach(z => {
bgx.fillStyle = z.color;
bgx.beginPath();
bgx.roundRect(z.x, top+4, z.w, bot-top-8, 14);
bgx.fill();
});
}
// ════════════════════════════════════════════════════════
// LAYOUT ENGINE
// ════════════════════════════════════════════════════════
const TB = 52, LH = 0, LP = 270, NW = 158, NH = 72;
function C(col) { // column center x — beginnt nach dem Log-Panel
const avail = innerWidth - LP;
return LP + avail * [0.11, 0.35, 0.62, 0.86][col];
}
function R(row) { // row center y
const avail = innerHeight - TB - LH - 16;
return TB + avail * [0.10, 0.34, 0.58, 0.82][row];
}
const NODE_POS = {
'n-svc': [0, 0], 'n-dash': [0, 1], 'n-nfy': [0, 2],
'n-ngx': [1, 0], 'n-be': [1, 1], 'n-db': [1, 2], 'n-fe': [1, 3],
'n-grp': [2, 0], 'n-ai': [2, 1], 'n-pmx': [2, 2],
'n-web': [3, 0], 'n-itune': [3, 2],
};
const ZONE_LABELS = {
'zl-pc': [0, 0.02], 'zl-srv': [1, 0.02],
'zl-ext': [2, 0.02], 'zl-adm': [3, 0.02],
};
const centers = {}; // id → {x, y}
function layout() {
Object.entries(NODE_POS).forEach(([id, [col, row]]) => {
const el = document.getElementById(id); if (!el) return;
const cx = C(col), cy = R(row);
el.style.left = (cx - NW/2) + 'px';
el.style.top = (cy - NH/2) + 'px';
centers[id] = { x: cx, y: cy };
});
Object.entries(ZONE_LABELS).forEach(([id, [col, rf]]) => {
const el = document.getElementById(id); if (!el) return;
const avail = innerHeight - TB;
el.style.left = (C(col) - 70) + 'px';
el.style.top = (TB + avail * rf + 4) + 'px';
});
}
// ════════════════════════════════════════════════════════
// CONNECTION PATHS
// ════════════════════════════════════════════════════════
const sv = document.getElementById('sv');
const connPaths = {}; // id → SVGPathElement
const CONNS = [
// id, from, to, color, curveDir, label
['ci_out', 'n-svc', 'n-ngx', '#3B82F6', 1, 'Check-in → nginx'],
['ci_in', 'n-ngx', 'n-svc', '#22C55E', -1, 'Response ← nginx'],
['ngx_be', 'n-ngx', 'n-be', '#10B981', 1, 'nginx → Backend'],
['be_ngx', 'n-be', 'n-ngx', '#10B981', -1, 'Backend → nginx'],
['be_db', 'n-be', 'n-db', '#6B7280', 1, 'Backend → SQLite'],
['db_be', 'n-db', 'n-be', '#6B7280', -1, 'SQLite → Backend'],
['dash_read','n-dash', 'n-svc', '#6366F1', 1, 'Dashboard liest status.json'],
['be_grp', 'n-be', 'n-grp', '#818CF8', 0, 'Backend → MS Graph'],
['be_ai', 'n-be', 'n-ai', '#2DD4BF', 0, 'Backend → Claude AI'],
['be_pmx', 'n-be', 'n-pmx', '#F97316', 0, 'Backend → Proxmox'],
['web_ngx', 'n-web', 'n-ngx', '#E4E4E7', -1, 'Admin → nginx'],
['ngx_fe', 'n-ngx', 'n-fe', '#3B82F6', -1, 'nginx → Frontend'],
['fe_web', 'n-fe', 'n-web', '#3B82F6', 1, 'Frontend → Browser'],
['ann', 'n-svc', 'n-nfy', '#F59E0B', 1, 'Ankündigung → Notification'],
['cmd', 'n-be', 'n-svc', '#EF4444', -1, 'Command → Service'],
['itune', 'n-itune','n-svc', '#0EA5E9', -1, 'Intune → Service (Deploy)'],
];
function nodeEdge(fromId, toId) {
const f = centers[fromId], t = centers[toId];
if (!f || !t) return { fx:0,fy:0, tx:0,ty:0 };
const dx = t.x-f.x, dy = t.y-f.y;
const ang = Math.atan2(dy, dx);
const hw = NW/2+4, hh = NH/2+4;
// From
const sf = Math.min(hw/Math.abs(Math.cos(ang)||1e-9), hh/Math.abs(Math.sin(ang)||1e-9));
const fx = f.x + Math.cos(ang)*Math.min(sf, Math.hypot(hw,hh));
const fy = f.y + Math.sin(ang)*Math.min(sf, Math.hypot(hw,hh));
// To
const a2 = ang+Math.PI;
const st = Math.min(hw/Math.abs(Math.cos(a2)||1e-9), hh/Math.abs(Math.sin(a2)||1e-9));
const tx = t.x + Math.cos(a2)*Math.min(st, Math.hypot(hw,hh));
const ty = t.y + Math.sin(a2)*Math.min(st, Math.hypot(hw,hh));
return { fx, fy, tx, ty };
}
function pathD(fx, fy, tx, ty, curveDir) {
const dx = tx-fx, dy = ty-fy;
const len = Math.hypot(dx,dy)||1;
// Perpendicular offset
const nx = -dy/len, ny = dx/len;
const curve = len * 0.22 * curveDir;
const cx = (fx+tx)/2 + nx*curve;
const cy = (fy+ty)/2 + ny*curve;
return { d: `M${fx},${fy} Q${cx},${cy} ${tx},${ty}`, cx, cy };
}
// Pre-stored bezier control points for animation
const connData = {}; // id → { fx,fy,cx,cy,tx,ty, path }
function buildPaths() {
sv.querySelectorAll('.cp').forEach(e=>e.remove());
Object.keys(connPaths).forEach(k=>delete connPaths[k]);
CONNS.forEach(([id, fromId, toId, color, curveDir]) => {
const {fx,fy,tx,ty} = nodeEdge(fromId, toId);
const {d, cx, cy} = pathD(fx,fy,tx,ty, curveDir);
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', d);
p.setAttribute('stroke', color+'1A');
p.setAttribute('stroke-width','1.5');
p.setAttribute('fill','none');
p.setAttribute('stroke-dasharray','3 10');
p.classList.add('cp');
sv.appendChild(p);
connPaths[id] = p;
connData[id] = { fx,fy,cx,cy,tx,ty, color, fromId, toId, curveDir };
});
}
// ════════════════════════════════════════════════════════
// PACKET SYSTEM
// ════════════════════════════════════════════════════════
let packets = [];
function quadBez(t, fx,fy,cx,cy,tx,ty) {
const m=1-t;
return { x: m*m*fx + 2*m*t*cx + t*t*tx, y: m*m*fy + 2*m*t*cy + t*t*ty };
}
function spawn(connId, color, speed, label, statKey, size=5) {
const d = connData[connId]; if(!d) return;
const {fx,fy,cx,cy,tx,ty,fromId,toId} = d;
// Glow trail
const trail = document.createElementNS('http://www.w3.org/2000/svg','circle');
trail.setAttribute('r', size*1.8);
trail.setAttribute('fill', color+'22');
// Core dot
const core = document.createElementNS('http://www.w3.org/2000/svg','circle');
core.setAttribute('r', size);
core.setAttribute('fill', color);
core.setAttribute('filter','url(#glow)');
// Bright center
const hi = document.createElementNS('http://www.w3.org/2000/svg','circle');
hi.setAttribute('r', size*0.45);
hi.setAttribute('fill','#ffffff88');
const g = document.createElementNS('http://www.w3.org/2000/svg','g');
g.append(trail,core,hi);
sv.appendChild(g);
// Highlight path
const pEl = connPaths[connId];
if(pEl) pEl.setAttribute('stroke', color+'50');
packets.push({ g, t:0, speed, fx,fy,cx,cy,tx,ty, color, pEl, toId, label, done:false });
addLog(color, label);
}
// ════════════════════════════════════════════════════════
// ANIMATION LOOP
// ════════════════════════════════════════════════════════
let lastT=0;
function frame(now) {
const dt = Math.min(now-(lastT||now), 50);
lastT=now;
packets = packets.filter(p => {
if(p.done){ p.g.remove(); return false; }
p.t = Math.min(p.t + p.speed*dt/1000, 1);
const pos = quadBez(p.t, p.fx,p.fy,p.cx,p.cy,p.tx,p.ty);
const pos2 = quadBez(Math.max(0,p.t-0.04), p.fx,p.fy,p.cx,p.cy,p.tx,p.ty);
const dx=pos.x-pos2.x, dy=pos.y-pos2.y;
p.g.setAttribute('transform',`translate(${pos.x},${pos.y})`);
// Rotate trail in direction of travel
p.g.children[0].setAttribute('cx', -dx*3);
p.g.children[0].setAttribute('cy', -dy*3);
if(p.t>=1){
if(p.pEl) p.pEl.setAttribute('stroke', connData[Object.keys(connData).find(k=>connData[k].fromId===connData[Object.keys(connData).find(k2=>connData[k2].fromId===p.pEl?.dataset?.from)])]?.color+'1A' || '#ffffff1A');
if(p.pEl) p.pEl.setAttribute('stroke', p.color+'1A');
if(p.toId) pulseNode(p.toId, p.color);
p.done=true;
}
return true;
});
requestAnimationFrame(frame);
}
function pulseNode(id, color) {
const el = document.getElementById(id); if(!el) return;
el.classList.add('lit');
el.style.setProperty('--nc', color);
el.style.setProperty('--ng', hexToRgba(color, 0.28));
el.style.setProperty('--ni', hexToRgba(color, 0.05));
setTimeout(()=>el.classList.remove('lit'), 700);
}
function hexToRgba(hex, a) {
const r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
return `rgba(${r},${g},${b},${a})`;
}
// ════════════════════════════════════════════════════════
// TRIGGER SCENARIOS
// ════════════════════════════════════════════════════════
function doCheckin() {
// 1. Agent schickt alle 60 Sek seine Daten an den Server
addLog('#3B82F6', '1/4 · Agent schickt Systemdaten an Server (CPU, RAM, Disk, BitLocker…)');
setTimeout(()=>spawn('ci_out','#3B82F6',.45,'Agent → nginx: Systemdaten senden',null), 0);
setTimeout(()=>spawn('ngx_be','#3B82F6',.50,'nginx → Backend: weiterleiten',null), 900);
// 2. Backend speichert in DB
setTimeout(()=>{
addLog('#6B7280','2/4 · Backend speichert Daten in SQLite-Datenbank');
spawn('be_db','#6B7280',.55,'Backend → SQLite: Gerätedaten speichern',null);
}, 1700);
// 3. Backend antwortet mit Commands / Ankündigungen
setTimeout(()=>{
addLog('#22C55E','3/4 · Server antwortet: keine neuen Befehle — alles OK');
spawn('be_ngx','#22C55E',.50,'Backend → nginx: Antwort mit Status',null);
}, 2600);
setTimeout(()=>spawn('ci_in','#22C55E',.45,'nginx → Agent: Antwort empfangen',null), 3400);
// 4. Agent schreibt status.json, Dashboard liest sie
setTimeout(()=>{
addLog('#6366F1','4/4 · Agent aktualisiert status.json · Dashboard zeigt neue Werte');
spawn('dash_read','#6366F1',.65,'Dashboard liest status.json (alle 5 Sek)',null);
pulseNode('n-dash','#6366F1');
}, 4200);
}
function doAnnouncement() {
addLog('#F59E0B','1/5 · Admin tippt Ankündigung im Web-Dashboard ein');
// Admin → Server
setTimeout(()=>spawn('web_ngx','#F59E0B',.43,'Browser → nginx: Ankündigung speichern',null,6), 0);
setTimeout(()=>spawn('ngx_be','#F59E0B',.50,'nginx → Backend: weiterleiten',null,6), 800);
setTimeout(()=>{
addLog('#F59E0B','2/5 · Backend speichert Ankündigung in Datenbank');
spawn('be_db','#F59E0B',.55,'Backend → SQLite: Ankündigung speichern',null,6);
}, 1500);
// Beim nächsten Check-in bekommt Agent die Ankündigung
setTimeout(()=>{
addLog('#F59E0B','3/5 · Beim nächsten Check-in: Agent empfängt Ankündigung');
spawn('ci_in','#F59E0B',.45,'nginx → Agent: Ankündigung mitschicken',null,6);
}, 3000);
// Agent startet Dialog als eingeloggter User
setTimeout(()=>{
addLog('#F59E0B','4/5 · Agent startet Popup-Dialog als eingeloggter Windows-User');
spawn('ann','#F59E0B',.38,'Agent → Notification-Fenster öffnen (schtasks)',null,6);
}, 4200);
setTimeout(()=>{
addLog('#22C55E','5/5 · User klickt "Gelesen" → Bestätigung geht zurück an Server ✅');
pulseNode('n-nfy','#22C55E');
spawn('ci_out','#22C55E',.43,'Agent → Server: Ankündigung bestätigt (ACK)',null,6);
}, 6500);
}
function doPatch() {
addLog('#EF4444','1/5 · Admin klickt "Windows Updates installieren" im Patch-Management');
setTimeout(()=>spawn('web_ngx','#EF4444',.43,'Browser → nginx: Patch-Befehl senden',null,6), 0);
setTimeout(()=>spawn('ngx_be','#EF4444',.50,'nginx → Backend: weiterleiten',null,6), 800);
setTimeout(()=>{
addLog('#EF4444','2/5 · Backend schreibt Befehl in die Datenbank (Command-Queue)');
spawn('be_db','#EF4444',.55,'Backend → SQLite: Befehl in Queue eintragen',null,6);
}, 1500);
setTimeout(()=>{
addLog('#EF4444','3/5 · Beim nächsten Check-in (max. 60 Sek) bekommt Agent den Befehl');
spawn('ci_in','#EF4444',.45,'nginx → Agent: Befehl "install_updates" mitschicken',null,6);
}, 4000);
setTimeout(()=>{
addLog('#EF4444','4/5 · Agent führt Windows Updates aus (UsoClient.exe)…');
pulseNode('n-svc','#EF4444');
}, 5200);
setTimeout(()=>{
addLog('#22C55E','5/5 · Agent meldet Ergebnis zurück: "done" ✅');
spawn('ci_out','#22C55E',.43,'Agent → Server: Ergebnis melden (done)',null,6);
}, 8000);
}
function doUpdate() {
addLog('#2DD4BF','1/4 · Server hat neue Agent-Version — Agent merkt das beim Check-in');
setTimeout(()=>spawn('ci_in','#2DD4BF',.45,'nginx → Agent: "neue Version v2.1.0 verfügbar"',null,6), 200);
setTimeout(()=>{
addLog('#2DD4BF','2/4 · Agent lädt neue EXE vom Server herunter');
spawn('cmd','#2DD4BF',.32,'Agent → Server: neue EXE herunterladen',null,7);
}, 1400);
setTimeout(()=>{
addLog('#2DD4BF','3/4 · Agent stoppt Service, ersetzt EXE-Datei, startet Service neu');
pulseNode('n-svc','#2DD4BF');
}, 4500);
setTimeout(()=>{
addLog('#22C55E','4/4 · Update fertig — Agent läuft mit neuer Version ✅');
spawn('ci_out','#22C55E',.43,'Agent → Server: Check-in mit neuer Version',null,6);
}, 6000);
}
function doIntune() {
addLog('#0EA5E9','1/3 · Intune (MDM) erkennt: Agent fehlt auf diesem Gerät');
setTimeout(()=>{
addLog('#0EA5E9','2/3 · Intune schiebt setup.exe aufs Gerät und führt sie aus');
spawn('itune','#0EA5E9',.28,'Intune → Gerät: setup.exe installieren',null,7);
}, 500);
setTimeout(()=>{
addLog('#0EA5E9',' Installer: Service registrieren · Autostart · Start-Menü Eintrag');
pulseNode('n-svc','#0EA5E9');
pulseNode('n-dash','#0EA5E9');
}, 5000);
setTimeout(()=>{
addLog('#22C55E','3/3 · Agent startet, meldet sich beim Server an ✅');
spawn('ci_out','#22C55E',.43,'Agent → Server: erster Check-in nach Installation',null,6);
}, 6500);
}
// ════════════════════════════════════════════════════════
// EVENT LOG
// ════════════════════════════════════════════════════════
const logLines = [];
function addLog(color, msg) {
const t = new Date().toLocaleTimeString('de-DE',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
const stepMatch = msg.match(/^(\d+\/\d+)\s*·\s*(.*)/);
const step = stepMatch ? stepMatch[1] : '';
const text = stepMatch ? stepMatch[2] : msg;
logLines.unshift({t, step, text, color});
if(logLines.length>6) logLines.pop();
document.getElementById('log-body').innerHTML = logLines.map((l,i) => {
const opacity = i === 0 ? '1' : i === 1 ? '0.7' : i === 2 ? '0.5' : '0.3';
return `<div class="ll" style="--lc:${l.color};opacity:${opacity}">
<div class="ll-t">${l.t}${l.step ? ` &nbsp;·&nbsp; <b>${l.step}</b>` : ''}</div>
<div class="ll-m" style="color:${l.color}">${l.text}</div>
</div>`;
}).join('');
}
// ════════════════════════════════════════════════════════
// INIT + RESIZE
// ════════════════════════════════════════════════════════
function init() {
layout();
drawGrid();
buildPaths();
}
window.addEventListener('resize', ()=>{
clearTimeout(window._rt);
window._rt = setTimeout(()=>{ init(); },80);
});
window.addEventListener('load', ()=>{
setTimeout(()=>{
init();
requestAnimationFrame(frame);
}, 120);
});
init();
</script>
</body>
</html>

80
agent-cs/setup.iss Normal file
View File

@@ -0,0 +1,80 @@
#define MyAppName "IT Nexus Agent"
#define MyAppVersion "2.0.0"
#define MyAppPublisher "Cereda Systems GmbH"
#define MyAppURL "https://it-nexus.cereda-systems.de"
#define MyAppExeName "IT-Nexus-Agent.exe"
#define MyAppGUID "B3F7A2C1-4E8D-4F2A-9B1C-7D3E5F6A8B2C"
[Setup]
AppId={{{#MyAppGUID}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
DisableWelcomePage=no
OutputDir=..\installer
OutputBaseFilename=IT-Nexus-Agent-Setup-{#MyAppVersion}
SetupIconFile=icon.ico
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
WizardResizable=no
PrivilegesRequired=admin
MinVersion=10.0.17763
ArchitecturesInstallIn64BitMode=x64
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName} {#MyAppVersion}
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppName}
CloseApplications=yes
CloseApplicationsFilter=IT-Nexus-Agent.exe
[Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Files]
Source: "bin\Publish\IT-Nexus-Agent.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "icon.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "config.template.json"; DestDir: "{commonappdata}\IT Nexus Agent"; DestName: "config.json"; Flags: onlyifdoesntexist uninsneveruninstall
[Icons]
; Kein Start-Menü-Icon und kein Desktop-Icon — Agent läuft unsichtbar im Hintergrund
[Registry]
; Kein Autostart-Eintrag — Dashboard wird NICHT automatisch gestartet
[Run]
Filename: "{app}\{#MyAppExeName}"; Parameters: "--install"; Flags: runhidden waituntilterminated; StatusMsg: "Installiere Windows Service..."
[UninstallRun]
Filename: "{app}\{#MyAppExeName}"; Parameters: "--uninstall"; Flags: runhidden waituntilterminated
[UninstallDelete]
Type: files; Name: "{commonappdata}\IT Nexus Agent\status.json"
Type: files; Name: "{commonappdata}\IT Nexus Agent\agent.log"
[Code]
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
begin
if CurStep = ssInstall then begin
// Service stoppen (neue Architektur)
Exec('net.exe', 'stop "IT Nexus Agent"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// Alte Scheduled Tasks (v1.x PowerShell-Agent) entfernen
Exec('schtasks.exe', '/delete /tn "IT Nexus Agent" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "IT Nexus Agent Watcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "ITNexusAgent" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Exec('schtasks.exe', '/delete /tn "ITNexusAgentWatcher" /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// Autostart-Eintrag aus vorheriger Version entfernen (falls vorhanden)
RegDeleteValue(HKCU, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'IT Nexus Agent');
Sleep(2000);
end;
end;

671
agent-cs/topology.html Normal file
View File

@@ -0,0 +1,671 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>IT Nexus — Systemarchitektur</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0A0A0C;
color: #E4E4E7;
min-height: 100vh;
padding: 40px 32px;
}
h1 {
font-size: 22px;
font-weight: 700;
color: #FAFAFA;
margin-bottom: 4px;
}
.subtitle {
font-size: 13px;
color: #52525B;
margin-bottom: 40px;
}
.subtitle span {
color: #009B9A;
font-weight: 600;
}
.topology {
display: grid;
grid-template-columns: 1fr 60px 1fr 60px 1fr;
grid-template-rows: auto;
gap: 0;
align-items: start;
max-width: 1200px;
}
/* ─── COLUMNS ─── */
.col { display: flex; flex-direction: column; gap: 12px; }
/* ─── NODES ─── */
.node {
background: #18181B;
border: 1px solid #27272A;
border-radius: 14px;
overflow: hidden;
}
.node-header {
padding: 12px 16px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #27272A;
}
.node-icon {
width: 32px; height: 32px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.node-title { font-size: 13px; font-weight: 700; color: #FAFAFA; }
.node-sub { font-size: 10px; color: #52525B; margin-top: 1px; }
.node-body { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
/* ─── COMPONENTS inside nodes ─── */
.comp {
background: #1C1C1F;
border: 1px solid #2A2A2E;
border-radius: 9px;
padding: 9px 12px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.comp-icon {
font-size: 15px;
margin-top: 1px;
flex-shrink: 0;
}
.comp-name { font-size: 12px; font-weight: 600; color: #E4E4E7; }
.comp-desc { font-size: 10px; color: #52525B; margin-top: 2px; line-height: 1.4; }
.comp-badge {
display: inline-block;
font-size: 9px;
font-weight: 600;
padding: 1px 6px;
border-radius: 4px;
margin-top: 4px;
}
.badge-green { background: #14532D; color: #4ADE80; }
.badge-blue { background: #1E3A5F; color: #60A5FA; }
.badge-orange { background: #431407; color: #FB923C; }
.badge-teal { background: #0D3030; color: #2DD4BF; }
.badge-red { background: #450A0A; color: #F87171; }
.badge-gray { background: #27272A; color: #71717A; }
/* ─── ARROWS / CONNECTORS ─── */
.arrow-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 60px;
gap: 0;
}
.arrow-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
margin-bottom: 20px;
}
.arrow-label {
font-size: 9px;
color: #3F3F46;
text-align: center;
max-width: 52px;
line-height: 1.3;
}
.arrow-line {
width: 1px;
height: 28px;
background: linear-gradient(to bottom, #27272A, #3F3F46);
}
.arrow-head {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #3F3F46;
}
.arrow-both { display: flex; flex-direction: column; align-items: center; }
.arrow-up { border-top: none; border-bottom: 6px solid #3F3F46; }
.arrow-bi { color: #3F3F46; font-size: 14px; }
/* ─── COLUMN HEADERS ─── */
.col-label {
font-size: 10px;
font-weight: 700;
color: #3F3F46;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
padding-left: 4px;
}
/* ─── FLOW LEGEND ─── */
.legend {
max-width: 1200px;
margin-top: 36px;
background: #18181B;
border: 1px solid #27272A;
border-radius: 14px;
padding: 20px 24px;
}
.legend h2 {
font-size: 12px;
font-weight: 700;
color: #52525B;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 14px;
}
.flow-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.flow-item {
background: #1C1C1F;
border: 1px solid #27272A;
border-radius: 9px;
padding: 12px 14px;
}
.flow-title { font-size: 11px; font-weight: 600; color: #E4E4E7; margin-bottom: 4px; }
.flow-desc { font-size: 10px; color: #52525B; line-height: 1.5; }
.flow-step { display: flex; align-items: baseline; gap: 6px; }
.flow-num { font-size: 9px; font-weight: 700; color: #009B9A; }
/* colors per zone */
.zone-pc .node-header { background: #0F1B2D; }
.zone-pc .node-icon { background: #1E3A5F; }
.zone-server .node-header { background: #0A1F0A; }
.zone-server .node-icon { background: #14532D; }
.zone-admin .node-header { background: #1A0F2E; }
.zone-admin .node-icon { background: #2E1065; }
</style>
</head>
<body>
<h1>IT Nexus — Systemarchitektur</h1>
<p class="subtitle">v2.0.0 &nbsp;·&nbsp; <span>Cereda Systems GmbH</span> &nbsp;·&nbsp; Stand: Mai 2026</p>
<div class="topology">
<!-- ═══ SPALTE 1: Windows Client ═══ -->
<div class="col zone-pc">
<div class="col-label">Windows-Endgerät</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🖥️</div>
<div>
<div class="node-title">IT-Nexus-Agent.exe</div>
<div class="node-sub">C:\Program Files\IT Nexus Agent\</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">⚙️</div>
<div>
<div class="comp-name">Windows Service</div>
<div class="comp-desc">Läuft als SYSTEM · Autostart<br>Check-in alle 60 Sekunden</div>
<span class="comp-badge badge-green">Running</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">📊</div>
<div>
<div class="comp-name">Dashboard (-dashboard)</div>
<div class="comp-desc">WPF Dark-Theme · System Tray<br>Autostart bei Windows-Login</div>
<span class="comp-badge badge-blue">STAThread</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">🔔</div>
<div>
<div class="comp-name">Ankündigung (-notify)</div>
<div class="comp-desc">WPF Dialog per schtasks.exe<br>Startet als eingeloggter User</div>
<span class="comp-badge badge-orange">On Demand</span>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">📦</div>
<div>
<div class="node-title">Gesammelte Daten</div>
<div class="node-sub">status.json · agent.log · config.json</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">🔒</div>
<div>
<div class="comp-name">BitLocker · Defender</div>
<div class="comp-desc">WMI-Abfragen als SYSTEM<br>Win32_EncryptableVolume · MSFT_MpComputerStatus</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">📈</div>
<div>
<div class="comp-name">CPU · RAM · Disk · Updates</div>
<div class="comp-desc">Performance Counter · WMI<br>Windows Update COM API</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🏷️</div>
<div>
<div class="comp-name">Hardware · Software · TPM</div>
<div class="comp-desc">BIOS Serial · Registry Uninstall<br>Win32_Tpm · Secure Boot</div>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🔧</div>
<div>
<div class="node-title">Installer (setup.exe)</div>
<div class="node-sub">Inno Setup 6 · GUID {B3F7A2C1…}</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">📋</div>
<div>
<div class="comp-name">Was der Installer macht</div>
<div class="comp-desc">
EXE → Program Files<br>
sc.exe create → Windows Service<br>
HKCU\Run → Autostart Dashboard<br>
Start-Menü Verknüpfung<br>
config.json (falls nicht vorhanden)<br>
Erkennungsregel für Intune
</div>
<span class="comp-badge badge-teal">Intune-kompatibel</span>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ ARROWS 1→2 ═══ -->
<div class="arrow-col">
<div class="arrow-group">
<div class="arrow-label">POST /checkin<br>X-Agent-Key</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-group">
<div class="arrow-label">Response<br>commands<br>anns</div>
<div class="arrow-line" style="background:linear-gradient(to bottom,#3F3F46,#27272A)"></div>
<div class="arrow-head arrow-up" style="border-bottom-color:#3F3F46;border-top:none"></div>
</div>
<div class="arrow-group">
<div class="arrow-label">HTTPS 443</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-group">
<div class="arrow-label">GET<br>/agent-script</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
</div>
<!-- ═══ SPALTE 2: Server ═══ -->
<div class="col zone-server">
<div class="col-label">IT Nexus Server · 192.168.0.194</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🐳</div>
<div>
<div class="node-title">Docker Stack</div>
<div class="node-sub">LXC 102 · Ubuntu 24.04 · docker compose</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">🌐</div>
<div>
<div class="comp-name">nginx (Reverse Proxy)</div>
<div class="comp-desc">Port 80 → 443 · TLS · Let's Encrypt<br>Leitet /api/* → Backend · /* → Frontend</div>
<span class="comp-badge badge-green">Port 443</span>
</div>
</div>
<div class="comp">
<div class="comp-icon"></div>
<div>
<div class="comp-name">fido-backend (Node.js)</div>
<div class="comp-desc">Express · better-sqlite3<br>JWT Auth · Agent-Key Auth<br>Cron Jobs · KI (Claude claude-sonnet-4-6)</div>
<span class="comp-badge badge-blue">Port 5000</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">⚛️</div>
<div>
<div class="comp-name">fido-frontend (React)</div>
<div class="comp-desc">CRA · Custom CSS<br>Monitoring · Patch · Helpdesk<br>KI-Assistent · Docs</div>
<span class="comp-badge badge-blue">Port 3000</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">🗄️</div>
<div>
<div class="comp-name">SQLite Datenbank</div>
<div class="comp-desc">monitoring_agents · tickets · users<br>patch_commands · announcements</div>
<span class="comp-badge badge-gray">Volume-Mount</span>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🔗</div>
<div>
<div class="node-title">Externe Dienste</div>
<div class="node-sub">Integrationen via Backend</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">☁️</div>
<div>
<div class="comp-name">Microsoft Graph API</div>
<div class="comp-desc">Azure AD · Teams · MDO Alerts<br>Mail.ReadWrite · SecurityAlert.Read.All</div>
<span class="comp-badge badge-blue">App Registration</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">🤖</div>
<div>
<div class="comp-name">Anthropic Claude claude-sonnet-4-6</div>
<div class="comp-desc">KI-Assistent · Wissensdatenbank<br>Ticket-Analyse · Automatisierung</div>
<span class="comp-badge badge-teal">claude-sonnet-4-6</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">🖥️</div>
<div>
<div class="comp-name">Proxmox API</div>
<div class="comp-desc">192.168.0.184 · hve-01<br>VM/CT Status · Ressourcen</div>
<span class="comp-badge badge-orange">API Token</span>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">📡</div>
<div>
<div class="node-title">Agent-Endpunkte</div>
<div class="node-sub">Kein JWT — nur X-Agent-Key Header</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">📤</div>
<div>
<div class="comp-name">POST /api/monitoring/checkin</div>
<div class="comp-desc">Systemdaten · Bekommt commands + announcements zurück</div>
</div>
</div>
<div class="comp">
<div class="comp-icon"></div>
<div>
<div class="comp-name">POST /api/patch/commands/result</div>
<div class="comp-desc">Ergebnis von Patch-Befehlen melden</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🔔</div>
<div>
<div class="comp-name">POST /api/announcements/:id/ack-agent</div>
<div class="comp-desc">Bestätigung nach Klick im Dialog</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">⬇️</div>
<div>
<div class="comp-name">GET /api/monitoring/agent-script</div>
<div class="comp-desc">Neue EXE-Version für Auto-Update</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ ARROWS 2→3 ═══ -->
<div class="arrow-col">
<div class="arrow-group">
<div class="arrow-label">HTTPS<br>Browser</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-group">
<div class="arrow-label">JWT<br>Auth</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="arrow-group">
<div class="arrow-label">Real-time<br>Dashboard</div>
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
</div>
<!-- ═══ SPALTE 3: Admin / Browser ═══ -->
<div class="col zone-admin">
<div class="col-label">IT-Administrator</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🧑‍💼</div>
<div>
<div class="node-title">IT Nexus Web</div>
<div class="node-sub">https://it-nexus.cereda-systems.de</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">📊</div>
<div>
<div class="comp-name">Monitoring</div>
<div class="comp-desc">Alle Geräte · Online/Offline<br>CPU/RAM/Disk · BitLocker · Defender<br>Seriennummer · Patch-Status</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🔄</div>
<div>
<div class="comp-name">Patch Management</div>
<div class="comp-desc">Staged Rollout: Test → Pilot → Produktion<br>Commands: Updates, Reboot, Win11<br>Auto-Update Agent</div>
<span class="comp-badge badge-orange">Rollout-gesichert</span>
</div>
</div>
<div class="comp">
<div class="comp-icon">📢</div>
<div>
<div class="comp-name">Ankündigungen</div>
<div class="comp-desc">Erstellen · An Gruppen senden<br>Agent zeigt WPF-Dialog auf Endgerät<br>ACK-Tracking</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🎫</div>
<div>
<div class="comp-name">Helpdesk</div>
<div class="comp-desc">Tickets · Chat · KI-Assistent<br>Teams-Integration</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🤖</div>
<div>
<div class="comp-name">KI-Wissensdatenbank</div>
<div class="comp-desc">Artikel · Auto-Sync alle 30s<br>Claude claude-sonnet-4-6 Analyse</div>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🏢</div>
<div>
<div class="node-title">Infrastruktur-Übersicht</div>
<div class="node-sub">Proxmox · LXC · VMs</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">🖥️</div>
<div>
<div class="comp-name">Proxmox hve-01</div>
<div class="comp-desc">192.168.0.184 · LXC 102 (IT Nexus)<br>VM/CT Status via REST API</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">💾</div>
<div>
<div class="comp-name">Synology NAS</div>
<div class="comp-desc">192.168.0.60 · iSCSI 500GB<br>iscsi-lvm auf Proxmox</div>
</div>
</div>
</div>
</div>
<div class="node">
<div class="node-header">
<div class="node-icon">🔐</div>
<div>
<div class="node-title">Rollen &amp; Zugriffsschutz</div>
<div class="node-sub">JWT · Rollenbasiertes Routing</div>
</div>
</div>
<div class="node-body">
<div class="comp">
<div class="comp-icon">👑</div>
<div>
<div class="comp-name">super_admin / admin</div>
<div class="comp-desc">Vollzugriff · alle Bereiche</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🛠️</div>
<div>
<div class="comp-name">support / bearbeiter</div>
<div class="comp-desc">Helpdesk · IT-Verwaltung</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">👤</div>
<div>
<div class="comp-name">benutzer / hr / produktion</div>
<div class="comp-desc">Eingeschränkt · nur eigene Tickets</div>
</div>
</div>
<div class="comp">
<div class="comp-icon">🖥️</div>
<div>
<div class="comp-name">Dashboard Admin-Modus</div>
<div class="comp-desc">Login direkt am Endgerät<br>Agent-Steuerung · Log-Ansicht</div>
<span class="comp-badge badge-red">Auth geschützt</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ ABLAUF-LEGENDE ═══ -->
<div class="legend">
<h2>Wichtige Abläufe</h2>
<div class="flow-grid">
<div class="flow-item">
<div class="flow-title">🔄 Normaler Check-in (60s)</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>Service sammelt Systemdaten via WMI + Registry</div>
<div class="flow-step"><span class="flow-num">2</span>POST /api/monitoring/checkin mit X-Agent-Key</div>
<div class="flow-step"><span class="flow-num">3</span>Server antwortet: commands + announcements</div>
<div class="flow-step"><span class="flow-num">4</span>status.json → Dashboard liest alle 5s</div>
</div>
</div>
<div class="flow-item">
<div class="flow-title">📢 Ankündigung senden</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>Admin erstellt Ankündigung im Web-Frontend</div>
<div class="flow-step"><span class="flow-num">2</span>Beim nächsten Checkin: Agent empfängt Announcement</div>
<div class="flow-step"><span class="flow-num">3</span>Service startet via schtasks.exe den Dialog als User</div>
<div class="flow-step"><span class="flow-num">4</span>User klickt "Gelesen" → ACK an Server</div>
</div>
</div>
<div class="flow-item">
<div class="flow-title">🔄 Auto-Update Agent</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>Server-Response enthält neue agent_version</div>
<div class="flow-step"><span class="flow-num">2</span>Agent lädt neue EXE via GET /agent-script</div>
<div class="flow-step"><span class="flow-num">3</span>CMD-Script: Service stop → EXE ersetzen → Service start</div>
<div class="flow-step"><span class="flow-num">4</span>Staged Rollout: Test → Pilot → Produktion</div>
</div>
</div>
<div class="flow-item">
<div class="flow-title">🔧 Patch-Command ausführen</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>Admin sendet Befehl im Patch-Management</div>
<div class="flow-step"><span class="flow-num">2</span>Command kommt beim nächsten Checkin zurück</div>
<div class="flow-step"><span class="flow-num">3</span>Service führt aus: UsoClient, shutdown, etc.</div>
<div class="flow-step"><span class="flow-num">4</span>POST /commands/result meldet done/error</div>
</div>
</div>
<div class="flow-item">
<div class="flow-title">📦 Intune-Deployment</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>setup.exe via Intune Win32-App verteilen</div>
<div class="flow-step"><span class="flow-num">2</span>Erkennungsregel: HKLM …\{B3F7A2C1…} DisplayVersion</div>
<div class="flow-step"><span class="flow-num">3</span>Installer: Service + Autostart + config.json</div>
<div class="flow-step"><span class="flow-num">4</span>Agent meldet sich selbstständig beim Server an</div>
</div>
</div>
<div class="flow-item">
<div class="flow-title">🔐 Dashboard Exit-Schutz</div>
<div class="flow-desc">
<div class="flow-step"><span class="flow-num">1</span>X-Button → minimiert nur in Tray, schließt nie</div>
<div class="flow-step"><span class="flow-num">2</span>"Beenden" im Tray → Admin-Login-Modal</div>
<div class="flow-step"><span class="flow-num">3</span>POST /api/auth/login → JWT-Token</div>
<div class="flow-step"><span class="flow-num">4</span>Nur bei Erfolg: Shutdown()</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,33 @@
# IT Nexus Agent deinstallieren (als Admin ausführen!)
Write-Host "Stoppe IT Nexus Agent Service..." -ForegroundColor Yellow
# Dashboard-Prozess killen
Get-Process -Name "IT-Nexus-Agent" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Milliseconds 500
# Service stoppen
net stop "IT Nexus Agent" 2>$null
Start-Sleep -Seconds 2
# Service löschen
sc.exe delete "IT Nexus Agent"
Start-Sleep -Seconds 1
# Autostart entfernen
Remove-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "IT Nexus Agent" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "IT Nexus Agent" -ErrorAction SilentlyContinue
# Alten Installations-Ordner falls vorhanden entfernen
if (Test-Path "C:\Program Files\IT Nexus Agent") {
Remove-Item "C:\Program Files\IT Nexus Agent" -Recurse -Force
Write-Host "Installations-Ordner entfernt" -ForegroundColor Green
}
# Status-Datei bleibt (config.json behalten, status.json löschen)
Remove-Item "C:\ProgramData\IT Nexus Agent\status.json" -ErrorAction SilentlyContinue
Write-Host "status.json entfernt"
Write-Host ""
Write-Host "Agent vollstaendig entfernt. Jetzt setup.exe ausfuehren!" -ForegroundColor Green
Write-Host "Druecke Enter zum Schliessen..." -ForegroundColor Gray
Read-Host

72
agent-cs/update-now.ps1 Normal file
View File

@@ -0,0 +1,72 @@
# IT Nexus Agent — Sofort-Update auf neues EXE (als Admin ausführen)
$newExe = "$PSScriptRoot\bin\Release\net8.0-windows\IT-Nexus-Agent.exe"
$svcExe = "C:\ProgramData\IT Nexus Agent\IT-Nexus-Agent.exe"
$pfExe = "C:\Program Files\IT Nexus Agent\IT-Nexus-Agent.exe"
$svcName = "IT Nexus Agent"
Write-Host "IT Nexus Agent — Update-Script" -ForegroundColor Cyan
Write-Host "Neue EXE: $newExe" -ForegroundColor Gray
if (-not (Test-Path $newExe)) {
Write-Host "FEHLER: Release-EXE nicht gefunden. Erst bauen!" -ForegroundColor Red
Read-Host; exit 1
}
# 1. Tray/Dashboard-Prozesse beenden
Write-Host "1. Stoppe Dashboard-Prozesse..." -ForegroundColor Yellow
Get-Process -Name "IT-Nexus-Agent" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Milliseconds 800
# 2. Service stoppen
Write-Host "2. Stoppe Windows Service..." -ForegroundColor Yellow
net stop $svcName 2>$null
Start-Sleep -Seconds 2
# 3. EXE an beiden Orten ersetzen
Write-Host "3. Kopiere neue EXE..." -ForegroundColor Yellow
$deps = @("IT-Nexus-Agent.dll","IT-Nexus-Agent.runtimeconfig.json",
"IT-Nexus-Agent.deps.json","Newtonsoft.Json.dll",
"System.Management.dll","System.ServiceProcess.ServiceController.dll")
foreach ($dest in @($svcExe, $pfExe)) {
if (Test-Path (Split-Path $dest)) {
Copy-Item $newExe $dest -Force
foreach ($d in $deps) {
$src = Join-Path (Split-Path $newExe) $d
if (Test-Path $src) { Copy-Item $src (Split-Path $dest) -Force }
}
Write-Host " OK: $dest" -ForegroundColor Green
}
}
# 4. Service-Binary-Pfad aktualisieren (falls nötig)
$regPath = (sc.exe qc $svcName 2>$null | Select-String "BINARY_PATH").ToString().Trim()
Write-Host "4. Aktueller Service-Pfad: $regPath" -ForegroundColor Gray
# Wenn Service auf ProgramData zeigt → auf Program Files umlenken
if ($regPath -like "*ProgramData*") {
Write-Host " Migriere Service-Pfad zu Program Files..." -ForegroundColor Yellow
sc.exe delete $svcName 2>$null
Start-Sleep -Milliseconds 500
sc.exe create $svcName binPath= "`"$pfExe`"" start= auto DisplayName= "IT Nexus Agent" 2>$null
sc.exe description $svcName "Cereda Systems IT Nexus Monitoring Agent" 2>$null
sc.exe failure $svcName reset= 60 actions= restart/5000/restart/10000/restart/30000 2>$null
}
# 5. Service starten
Write-Host "5. Starte Windows Service..." -ForegroundColor Yellow
net start $svcName 2>$null
Start-Sleep -Seconds 3
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($svc.Status -eq "Running") {
Write-Host ""
Write-Host "Update erfolgreich! Service laeuft." -ForegroundColor Green
Write-Host "Naechster Check-in in max. 60 Sekunden." -ForegroundColor Gray
} else {
Write-Host "WARNUNG: Service nicht gestartet. Status: $($svc.Status)" -ForegroundColor Red
}
Write-Host ""
Write-Host "Druecke Enter zum Schliessen..."
Read-Host