From 55e016c07d91007e573a20a4350e4112a1742623 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 19 Mar 2026 14:39:49 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=207=20=E2=80=94=20MeshCentral=20R?= =?UTF-8?q?emote=20Desktop=20Integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - MeshCentralOptions + MeshCentralService: Node-Lookup via Hostname, Remote-Desktop-URL-Generierung - RemoteDesktopController: GET /api/v1/agents/{id}/remote-session mit 3 Status-Zuständen (nicht konfiguriert / Agent fehlt / bereit) - Program.cs: HttpClient + MeshCentralService registriert, appsettings.json mit Konfigurationsblock Go Agent: - config.go: MeshCentralUrl + MeshEnabled Felder - internal/meshagent/installer.go: MeshAgent Download + Installation (Windows Service / Linux systemd) - main.go: Automatische MeshAgent-Installation nach Enrollment wenn aktiviert Frontend: - RemoteDesktopButton: Modales Dialog mit 3 Zustandsanzeigen (Setup nötig / Agent installieren / Remote Desktop öffnen) - AgentDetailPage: RemoteDesktopButton im Header integriert - api/types.ts + api/client.ts: RemoteSessionInfo Typ + remoteDesktopApi docker-compose.yml: MeshCentral Service (ghcr.io/ylianst/meshcentral:latest, Ports 4430/4431) Co-Authored-By: Claude Sonnet 4.6 --- Agent/cmd/agent/main.go | 9 ++ Agent/internal/config/config.go | 2 + Agent/internal/meshagent/installer.go | 149 ++++++++++++++++++ .../Controllers/RemoteDesktopController.cs | 72 +++++++++ Backend/src/NexusRMM.Api/Program.cs | 14 ++ .../Services/MeshCentralOptions.cs | 11 ++ .../Services/MeshCentralService.cs | 124 +++++++++++++++ Backend/src/NexusRMM.Api/appsettings.json | 7 + .../Migrations/RmmDbContextModelSnapshot.cs | 14 +- Frontend/src/api/client.ts | 7 + Frontend/src/api/types.ts | 11 ++ .../src/components/RemoteDesktopButton.tsx | 147 +++++++++++++++++ Frontend/src/pages/AgentDetailPage.tsx | 2 + docker-compose.yml | 17 ++ 14 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 Agent/internal/meshagent/installer.go create mode 100644 Backend/src/NexusRMM.Api/Controllers/RemoteDesktopController.cs create mode 100644 Backend/src/NexusRMM.Api/Services/MeshCentralOptions.cs create mode 100644 Backend/src/NexusRMM.Api/Services/MeshCentralService.cs create mode 100644 Frontend/src/components/RemoteDesktopButton.tsx diff --git a/Agent/cmd/agent/main.go b/Agent/cmd/agent/main.go index 512fed2..89892a9 100644 --- a/Agent/cmd/agent/main.go +++ b/Agent/cmd/agent/main.go @@ -15,6 +15,7 @@ import ( "nexusrmm.local/agent/internal/connection" "nexusrmm.local/agent/internal/deployer" "nexusrmm.local/agent/internal/executor" + "nexusrmm.local/agent/internal/meshagent" pb "nexusrmm.local/agent/pkg/proto" ) @@ -62,6 +63,14 @@ func main() { log.Printf("Enrolled with ID: %s", cfg.AgentID) } + // MeshAgent installieren falls konfiguriert + if cfg.MeshEnabled && cfg.MeshCentralUrl != "" { + log.Printf("Installiere MeshAgent von %s...", cfg.MeshCentralUrl) + if err := meshagent.Install(context.Background(), cfg.MeshCentralUrl); err != nil { + log.Printf("MeshAgent-Installation fehlgeschlagen (nicht kritisch): %v", err) + } + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigCh := make(chan os.Signal, 1) diff --git a/Agent/internal/config/config.go b/Agent/internal/config/config.go index 817535c..e293606 100644 --- a/Agent/internal/config/config.go +++ b/Agent/internal/config/config.go @@ -11,6 +11,8 @@ type Config struct { AgentID string `yaml:"agent_id"` HeartbeatInterval int `yaml:"heartbeat_interval"` TLSEnabled bool `yaml:"tls_enabled"` + MeshCentralUrl string `yaml:"mesh_central_url"` + MeshEnabled bool `yaml:"mesh_enabled"` } func Load(path string) (*Config, error) { diff --git a/Agent/internal/meshagent/installer.go b/Agent/internal/meshagent/installer.go new file mode 100644 index 0000000..063609b --- /dev/null +++ b/Agent/internal/meshagent/installer.go @@ -0,0 +1,149 @@ +package meshagent + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +// IsInstalled prüft ob der MeshAgent-Prozess läuft oder das Binary vorhanden ist. +func IsInstalled() bool { + if runtime.GOOS == "windows" { + // Prüfe ob MeshAgent Service läuft + cmd := exec.Command("sc", "query", "Mesh Agent") + return cmd.Run() == nil + } + // Linux: prüfe ob meshagent Prozess läuft + cmd := exec.Command("pgrep", "-x", "meshagent") + return cmd.Run() == nil +} + +// Install lädt den MeshAgent von MeshCentral herunter und installiert ihn. +// meshCentralUrl: z.B. "https://192.168.1.100:4430" +func Install(ctx context.Context, meshCentralUrl string) error { + if IsInstalled() { + log.Println("MeshAgent ist bereits installiert") + return nil + } + + log.Printf("MeshAgent wird von %s heruntergeladen...", meshCentralUrl) + + // Agent-ID je nach OS + agentID := "6" // Linux x64 + if runtime.GOOS == "windows" { + agentID = "3" // Windows x64 + } + + downloadUrl := fmt.Sprintf("%s/meshagents?id=%s", strings.TrimRight(meshCentralUrl, "/"), agentID) + + // SSL-Fehler ignorieren (selbstsigniertes Zertifikat in Dev) + httpClient := &http.Client{ + Timeout: 5 * time.Minute, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) + if err != nil { + return fmt.Errorf("HTTP Request konnte nicht erstellt werden: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("Download fehlgeschlagen: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("MeshCentral antwortete mit Status %d — ist MeshCentral gestartet?", resp.StatusCode) + } + + // Temp-Datei speichern + var tmpPath string + if runtime.GOOS == "windows" { + tmpPath = filepath.Join(os.TempDir(), "meshagent.exe") + } else { + tmpPath = filepath.Join(os.TempDir(), "meshagent") + } + + f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("Temp-Datei konnte nicht erstellt werden: %w", err) + } + if _, err := io.Copy(f, resp.Body); err != nil { + f.Close() + return fmt.Errorf("Fehler beim Schreiben: %w", err) + } + f.Close() + + log.Printf("MeshAgent heruntergeladen nach %s", tmpPath) + + // Installieren + return installBinary(ctx, tmpPath, meshCentralUrl) +} + +func installBinary(ctx context.Context, binaryPath, meshCentralUrl string) error { + if runtime.GOOS == "windows" { + // Windows: als Service installieren + cmd := exec.CommandContext(ctx, binaryPath, "-install", "-url", meshCentralUrl) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("MeshAgent Windows-Installation fehlgeschlagen: %w", err) + } + log.Println("MeshAgent als Windows Service installiert") + } else { + // Linux: in /usr/local/bin installieren und als Service starten + installPath := "/usr/local/bin/meshagent" + if err := os.Rename(binaryPath, installPath); err != nil { + // Falls rename scheitert (cross-device), kopieren + if err2 := copyFile(binaryPath, installPath); err2 != nil { + return fmt.Errorf("MeshAgent konnte nicht nach %s verschoben werden: %w", installPath, err2) + } + } + if err := os.Chmod(installPath, 0755); err != nil { + return fmt.Errorf("chmod fehlgeschlagen: %w", err) + } + + // Als Service starten (systemd oder direkt) + cmd := exec.CommandContext(ctx, installPath, "-install", "-url", meshCentralUrl) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + // Direkt starten als Fallback + log.Printf("Service-Installation fehlgeschlagen, starte direkt: %v", err) + go func() { + exec.Command(installPath, "-url", meshCentralUrl).Run() + }() + } + log.Println("MeshAgent auf Linux installiert") + } + return nil +} + +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer dstFile.Close() + + _, err = io.Copy(dstFile, srcFile) + return err +} diff --git a/Backend/src/NexusRMM.Api/Controllers/RemoteDesktopController.cs b/Backend/src/NexusRMM.Api/Controllers/RemoteDesktopController.cs new file mode 100644 index 0000000..1d1a828 --- /dev/null +++ b/Backend/src/NexusRMM.Api/Controllers/RemoteDesktopController.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NexusRMM.Api.Services; +using NexusRMM.Infrastructure.Data; + +namespace NexusRMM.Api.Controllers; + +[ApiController] +[Route("api/v1/agents/{agentId:guid}/remote-session")] +public class RemoteDesktopController : ControllerBase +{ + private readonly RmmDbContext _db; + private readonly MeshCentralService _meshCentral; + + public RemoteDesktopController(RmmDbContext db, MeshCentralService meshCentral) + { + _db = db; + _meshCentral = meshCentral; + } + + /// + /// Gibt Remote-Desktop-Informationen für einen Agent zurück. + /// Falls MeshCentral nicht konfiguriert ist, wird configured=false zurückgegeben. + /// + [HttpGet] + public async Task GetRemoteSession(Guid agentId) + { + var agent = await _db.Agents.FindAsync(agentId); + if (agent is null) return NotFound(); + + if (!_meshCentral.IsEnabled) + { + return Ok(new + { + configured = false, + message = "MeshCentral ist nicht konfiguriert. Setze 'MeshCentral:Enabled=true' in appsettings.json.", + setupUrl = "https://localhost:4430", + }); + } + + if (string.IsNullOrEmpty(agent.MeshAgentId)) + { + // Versuche Node zu finden + var nodeId = await _meshCentral.FindNodeByHostnameAsync(agent.Hostname); + if (nodeId is not null) + { + agent.MeshAgentId = nodeId; + await _db.SaveChangesAsync(); + } + } + + if (string.IsNullOrEmpty(agent.MeshAgentId)) + { + return Ok(new + { + configured = true, + agentInstalled = false, + message = $"MeshAgent nicht auf '{agent.Hostname}' gefunden. Agent muss MeshAgent installieren.", + meshAgentDownloadUrl = _meshCentral.GetMeshAgentDownloadUrl(agent.OsType.ToString()), + }); + } + + return Ok(new + { + configured = true, + agentInstalled = true, + meshNodeId = agent.MeshAgentId, + sessionUrl = _meshCentral.GetRemoteDesktopUrl(agent.MeshAgentId), + meshCentralBaseUrl = "https://localhost:4430", + }); + } +} diff --git a/Backend/src/NexusRMM.Api/Program.cs b/Backend/src/NexusRMM.Api/Program.cs index 10efc55..0ab5552 100644 --- a/Backend/src/NexusRMM.Api/Program.cs +++ b/Backend/src/NexusRMM.Api/Program.cs @@ -25,6 +25,20 @@ builder.Services.AddSwaggerGen(); builder.Services.AddScoped(); +// MeshCentral Konfiguration +builder.Services.Configure( + builder.Configuration.GetSection(MeshCentralOptions.SectionName)); + +// HttpClient für MeshCentral (mit optionalem SSL-Bypass für Entwicklung) +builder.Services.AddHttpClient("MeshCentral") + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true, // Dev: SSL bypass + UseCookies = false, + }); + +builder.Services.AddScoped(); + builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => diff --git a/Backend/src/NexusRMM.Api/Services/MeshCentralOptions.cs b/Backend/src/NexusRMM.Api/Services/MeshCentralOptions.cs new file mode 100644 index 0000000..d584cab --- /dev/null +++ b/Backend/src/NexusRMM.Api/Services/MeshCentralOptions.cs @@ -0,0 +1,11 @@ +namespace NexusRMM.Api.Services; + +public class MeshCentralOptions +{ + public const string SectionName = "MeshCentral"; + public string BaseUrl { get; set; } = "https://localhost:4430"; + public string Username { get; set; } = "admin"; + public string Password { get; set; } = "admin"; + public bool IgnoreSslErrors { get; set; } = true; + public bool Enabled { get; set; } = false; +} diff --git a/Backend/src/NexusRMM.Api/Services/MeshCentralService.cs b/Backend/src/NexusRMM.Api/Services/MeshCentralService.cs new file mode 100644 index 0000000..214344f --- /dev/null +++ b/Backend/src/NexusRMM.Api/Services/MeshCentralService.cs @@ -0,0 +1,124 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace NexusRMM.Api.Services; + +/// +/// HTTP-Client für die MeshCentral REST API. +/// Authentifizierung via Cookie-Session, dann Node-Lookup und Remote-Session-URL-Generierung. +/// +public class MeshCentralService +{ + private readonly MeshCentralOptions _options; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public MeshCentralService( + IOptions options, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + _options = options.Value; + _logger = logger; + _httpClient = httpClientFactory.CreateClient("MeshCentral"); + } + + public bool IsEnabled => _options.Enabled; + + /// + /// Gibt die Remote-Desktop-URL für einen MeshCentral-Node zurück. + /// Öffnet MeshCentral auf der Geräteseite — User muss im Browser eingeloggt sein. + /// + public string GetRemoteDesktopUrl(string meshNodeId) + { + // URL-Format für direkte Geräteansicht in MeshCentral + // viewmode=11 = Remote Desktop, hide=31 = UI Elemente verstecken + return $"{_options.BaseUrl.TrimEnd('/')}/?viewmode=11&hide=31#{Uri.EscapeDataString(meshNodeId)}"; + } + + /// + /// Sucht einen MeshCentral-Node anhand des Hostnamens. + /// Gibt die Node-ID zurück oder null wenn nicht gefunden. + /// + public async Task FindNodeByHostnameAsync(string hostname) + { + if (!_options.Enabled) return null; + + try + { + var loginCookie = await LoginAsync(); + if (loginCookie is null) return null; + + using var request = new HttpRequestMessage(HttpMethod.Get, + $"{_options.BaseUrl}/api/v1/devices"); + request.Headers.Add("Cookie", loginCookie); + + var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) return null; + + var content = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(content); + + // MeshCentral API gibt { devices: { nodeId: { name: "...", ... } } } zurück + if (doc.RootElement.TryGetProperty("devices", out var devices)) + { + foreach (var device in devices.EnumerateObject()) + { + if (device.Value.TryGetProperty("name", out var name) && + name.GetString()?.Equals(hostname, StringComparison.OrdinalIgnoreCase) == true) + { + return device.Name; // Node-ID + } + } + } + + return null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "MeshCentral Node-Lookup für {Hostname} fehlgeschlagen", hostname); + return null; + } + } + + /// + /// Gibt die Download-URL für den MeshAgent zurück. + /// Windows x64: id=3, Linux x64: id=6 + /// + public string GetMeshAgentDownloadUrl(string osType) + { + var agentId = osType.ToLower() == "windows" ? 3 : 6; + return $"{_options.BaseUrl.TrimEnd('/')}/meshagents?id={agentId}"; + } + + private async Task LoginAsync() + { + try + { + var loginData = new { username = _options.Username, password = _options.Password }; + var json = JsonSerializer.Serialize(loginData); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync( + $"{_options.BaseUrl}/api/v1/authToken", content); + + // MeshCentral gibt Set-Cookie zurück + if (response.Headers.TryGetValues("Set-Cookie", out var cookies)) + { + var sessionCookie = cookies.FirstOrDefault(c => c.StartsWith("meshcentral.sid")); + if (sessionCookie is not null) + return sessionCookie.Split(';')[0]; // Nur Name=Wert Teil + } + + return null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "MeshCentral Login fehlgeschlagen"); + return null; + } + } +} diff --git a/Backend/src/NexusRMM.Api/appsettings.json b/Backend/src/NexusRMM.Api/appsettings.json index 9720bbd..9141615 100644 --- a/Backend/src/NexusRMM.Api/appsettings.json +++ b/Backend/src/NexusRMM.Api/appsettings.json @@ -5,6 +5,13 @@ "Cors": { "Origins": ["http://localhost:5173"] }, + "MeshCentral": { + "BaseUrl": "https://localhost:4430", + "Username": "admin", + "Password": "admin", + "IgnoreSslErrors": true, + "Enabled": false + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/Backend/src/NexusRMM.Infrastructure/Migrations/RmmDbContextModelSnapshot.cs b/Backend/src/NexusRMM.Infrastructure/Migrations/RmmDbContextModelSnapshot.cs index 448ef54..2fd4749 100644 --- a/Backend/src/NexusRMM.Infrastructure/Migrations/RmmDbContextModelSnapshot.cs +++ b/Backend/src/NexusRMM.Infrastructure/Migrations/RmmDbContextModelSnapshot.cs @@ -74,7 +74,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("MacAddress"); - b.ToTable("Agents"); + b.ToTable("Agents", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.AgentMetric", b => @@ -100,7 +100,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("Timestamp"); - b.ToTable("AgentMetrics"); + b.ToTable("AgentMetrics", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.Alert", b => @@ -138,7 +138,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("RuleId"); - b.ToTable("Alerts"); + b.ToTable("Alerts", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.AlertRule", b => @@ -172,7 +172,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasKey("Id"); - b.ToTable("AlertRules"); + b.ToTable("AlertRules", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.SoftwarePackage", b => @@ -219,7 +219,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("Name", "Version", "OsType") .IsUnique(); - b.ToTable("SoftwarePackages"); + b.ToTable("SoftwarePackages", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.TaskItem", b => @@ -253,7 +253,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("AgentId"); - b.ToTable("Tasks"); + b.ToTable("Tasks", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.Ticket", b => @@ -291,7 +291,7 @@ namespace NexusRMM.Infrastructure.Migrations b.HasIndex("AgentId"); - b.ToTable("Tickets"); + b.ToTable("Tickets", (string)null); }); modelBuilder.Entity("NexusRMM.Core.Models.AgentMetric", b => diff --git a/Frontend/src/api/client.ts b/Frontend/src/api/client.ts index c16013c..2d8e5d4 100644 --- a/Frontend/src/api/client.ts +++ b/Frontend/src/api/client.ts @@ -14,6 +14,7 @@ import type { CreateSoftwarePackageRequest, DeployRequest, DeployResponse, + RemoteSessionInfo, } from './types' const BASE_URL = '/api/v1' @@ -97,3 +98,9 @@ export const deployApi = { deploy: (data: DeployRequest) => request('/deploy', { method: 'POST', body: JSON.stringify(data) }), } + +// Remote Desktop +export const remoteDesktopApi = { + getSession: (agentId: string) => + request(`/agents/${agentId}/remote-session`), +} diff --git a/Frontend/src/api/types.ts b/Frontend/src/api/types.ts index 2509d98..48d8e08 100644 --- a/Frontend/src/api/types.ts +++ b/Frontend/src/api/types.ts @@ -173,3 +173,14 @@ export interface DeployResponse { packageName: string version: string } + +export interface RemoteSessionInfo { + configured: boolean + agentInstalled?: boolean + message?: string + setupUrl?: string + meshAgentDownloadUrl?: string + meshNodeId?: string + sessionUrl?: string + meshCentralBaseUrl?: string +} diff --git a/Frontend/src/components/RemoteDesktopButton.tsx b/Frontend/src/components/RemoteDesktopButton.tsx new file mode 100644 index 0000000..21d0bbe --- /dev/null +++ b/Frontend/src/components/RemoteDesktopButton.tsx @@ -0,0 +1,147 @@ +import { useState } from 'react' +import { Monitor, Loader2, AlertTriangle, ExternalLink } from 'lucide-react' +import { remoteDesktopApi } from '../api/client' +import type { RemoteSessionInfo } from '../api/types' +import { cn } from '../lib/utils' + +interface RemoteDesktopButtonProps { + agentId: string + agentHostname: string + className?: string +} + +export function RemoteDesktopButton({ agentId, agentHostname, className }: RemoteDesktopButtonProps) { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [sessionInfo, setSessionInfo] = useState(null) + const [showModal, setShowModal] = useState(false) + + async function handleClick() { + setLoading(true) + setError(null) + try { + const info = await remoteDesktopApi.getSession(agentId) + setSessionInfo(info) + setShowModal(true) + } catch (e) { + setError('Remote-Session konnte nicht geladen werden') + } finally { + setLoading(false) + } + } + + function handleConnect() { + if (sessionInfo?.sessionUrl) { + window.open(sessionInfo.sessionUrl, `rmm-remote-${agentId}`, + 'width=1280,height=800,scrollbars=no,toolbar=no,menubar=no') + } + } + + return ( + <> + + + {/* Modal */} + {showModal && ( +
setShowModal(false)}> +
e.stopPropagation()}> +
+
+ +
+
+

Remote Desktop

+

{agentHostname}

+
+ +
+ + {/* Status: nicht konfiguriert */} + {sessionInfo && !sessionInfo.configured && ( +
+
+ +

{sessionInfo.message}

+
+

+ MeshCentral läuft unter{' '} + {sessionInfo.setupUrl}. + Richte MeshCentral ein und setze MeshCentral:Enabled=true in der appsettings.json. +

+ +
+ )} + + {/* Status: Agent nicht installiert */} + {sessionInfo?.configured && !sessionInfo.agentInstalled && ( +
+
+ +

{sessionInfo.message}

+
+

+ Der NexusRMM-Agent installiert MeshAgent automatisch wenn{' '} + mesh_enabled: true in der Agent-Config gesetzt ist. +

+ {sessionInfo.meshAgentDownloadUrl && ( + + )} +
+ )} + + {/* Status: bereit */} + {sessionInfo?.configured && sessionInfo.agentInstalled && ( +
+
+

✓ MeshAgent verbunden

+

Node ID: {sessionInfo.meshNodeId}

+
+

+ Remote Desktop öffnet sich in einem neuen Fenster. Falls du nach einem Login gefragt wirst, melde dich bei MeshCentral an. +

+ + +
+ )} + + {error && ( +

{error}

+ )} +
+
+ )} + + ) +} diff --git a/Frontend/src/pages/AgentDetailPage.tsx b/Frontend/src/pages/AgentDetailPage.tsx index 924c909..7dd8c36 100644 --- a/Frontend/src/pages/AgentDetailPage.tsx +++ b/Frontend/src/pages/AgentDetailPage.tsx @@ -10,6 +10,7 @@ import { AlertCircle, CheckCircle, } from 'lucide-react' +import { RemoteDesktopButton } from '../components/RemoteDesktopButton' import { LineChart, Line, @@ -170,6 +171,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { )}> {signalRStatus === 'connected' ? 'Live' : signalRStatus === 'reconnecting' ? 'Verbindet...' : 'Offline'} + diff --git a/docker-compose.yml b/docker-compose.yml index 29c8b61..3a5c099 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,23 @@ services: - nexusrmm_pgdata:/var/lib/postgresql/data restart: unless-stopped + meshcentral: + image: ghcr.io/ylianst/meshcentral:latest + container_name: nexusrmm-meshcentral + ports: + - "4430:4430" + - "4431:4431" + volumes: + - nexusrmm_meshdata:/opt/meshcentral/meshcentral-data + - nexusrmm_meshfiles:/opt/meshcentral/meshcentral-files + restart: unless-stopped + depends_on: + - nexusrmm-postgres + volumes: nexusrmm_pgdata: name: nexusrmm_pgdata + nexusrmm_meshdata: + name: nexusrmm_meshdata + nexusrmm_meshfiles: + name: nexusrmm_meshfiles