Initial commit: IT Nexus Web-App
This commit is contained in:
109
agent-cs/AgentService.cs
Normal file
109
agent-cs/AgentService.cs
Normal 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
195
agent-cs/AgentWorker.cs
Normal 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
8
agent-cs/GlobalUsings.cs
Normal 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;
|
||||
35
agent-cs/IT-Nexus-Agent.csproj
Normal file
35
agent-cs/IT-Nexus-Agent.csproj
Normal 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>
|
||||
91
agent-cs/Models/CheckinModels.cs
Normal file
91
agent-cs/Models/CheckinModels.cs
Normal 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
20
agent-cs/Models/Config.cs
Normal 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
58
agent-cs/Program.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
89
agent-cs/Services/ApiService.cs
Normal file
89
agent-cs/Services/ApiService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
123
agent-cs/Services/CommandExecutor.cs
Normal file
123
agent-cs/Services/CommandExecutor.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
110
agent-cs/Services/NotificationService.cs
Normal file
110
agent-cs/Services/NotificationService.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
64
agent-cs/Services/SecurityInfoService.cs
Normal file
64
agent-cs/Services/SecurityInfoService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
267
agent-cs/Services/SystemInfoService.cs
Normal file
267
agent-cs/Services/SystemInfoService.cs
Normal 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)];
|
||||
}
|
||||
}
|
||||
28
agent-cs/UI/AdminLoginDialog.xaml
Normal file
28
agent-cs/UI/AdminLoginDialog.xaml
Normal 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>
|
||||
65
agent-cs/UI/AdminLoginDialog.xaml.cs
Normal file
65
agent-cs/UI/AdminLoginDialog.xaml.cs
Normal 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;
|
||||
}
|
||||
510
agent-cs/UI/DashboardWindow.xaml
Normal file
510
agent-cs/UI/DashboardWindow.xaml
Normal 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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>
|
||||
503
agent-cs/UI/DashboardWindow.xaml.cs
Normal file
503
agent-cs/UI/DashboardWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
31
agent-cs/UI/LogWindow.xaml
Normal file
31
agent-cs/UI/LogWindow.xaml
Normal 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>
|
||||
40
agent-cs/UI/LogWindow.xaml.cs
Normal file
40
agent-cs/UI/LogWindow.xaml.cs
Normal 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 { }
|
||||
}
|
||||
}
|
||||
22
agent-cs/UI/MessageDialog.xaml
Normal file
22
agent-cs/UI/MessageDialog.xaml
Normal 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>
|
||||
18
agent-cs/UI/MessageDialog.xaml.cs
Normal file
18
agent-cs/UI/MessageDialog.xaml.cs
Normal 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;
|
||||
}
|
||||
108
agent-cs/UI/NotificationWindow.xaml
Normal file
108
agent-cs/UI/NotificationWindow.xaml
Normal 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>
|
||||
59
agent-cs/UI/NotificationWindow.xaml.cs
Normal file
59
agent-cs/UI/NotificationWindow.xaml.cs
Normal 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
22
agent-cs/app.manifest
Normal 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>
|
||||
4
agent-cs/config.template.json
Normal file
4
agent-cs/config.template.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"server_url": "https://it-nexus.cereda-systems.de",
|
||||
"agent_key": "HIER-DEN-AGENT-KEY-EINTRAGEN"
|
||||
}
|
||||
4
agent-cs/detect-it-nexus-agent.ps1
Normal file
4
agent-cs/detect-it-nexus-agent.ps1
Normal 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
BIN
agent-cs/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
629
agent-cs/network-topology.html
Normal file
629
agent-cs/network-topology.html
Normal 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 ? ` · <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
80
agent-cs/setup.iss
Normal 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
671
agent-cs/topology.html
Normal 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 · <span>Cereda Systems GmbH</span> · 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 & 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>
|
||||
33
agent-cs/uninstall-agent.ps1
Normal file
33
agent-cs/uninstall-agent.ps1
Normal 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
72
agent-cs/update-now.ps1
Normal 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
|
||||
Reference in New Issue
Block a user