feat: Phase 7 — MeshCentral Remote Desktop Integration

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 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-19 14:39:49 +01:00
parent 84629dfbcf
commit 55e016c07d
14 changed files with 579 additions and 7 deletions

View File

@@ -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<DeployResponse>('/deploy', { method: 'POST', body: JSON.stringify(data) }),
}
// Remote Desktop
export const remoteDesktopApi = {
getSession: (agentId: string) =>
request<RemoteSessionInfo>(`/agents/${agentId}/remote-session`),
}

View File

@@ -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
}

View File

@@ -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<string | null>(null)
const [sessionInfo, setSessionInfo] = useState<RemoteSessionInfo | null>(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 (
<>
<button
onClick={handleClick}
disabled={loading}
className={cn(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors',
'bg-purple-500/20 text-purple-400 border border-purple-500/30 hover:bg-purple-500/30',
loading && 'opacity-50 cursor-not-allowed',
className,
)}
>
{loading ? <Loader2 size={16} className="animate-spin" /> : <Monitor size={16} />}
Remote Desktop
</button>
{/* Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
onClick={() => setShowModal(false)}>
<div className="bg-card border border-border rounded-xl p-6 w-full max-w-md shadow-2xl"
onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-lg bg-purple-500/20 flex items-center justify-center">
<Monitor size={18} className="text-purple-400" />
</div>
<div>
<h3 className="font-semibold text-foreground">Remote Desktop</h3>
<p className="text-xs text-muted-foreground">{agentHostname}</p>
</div>
<button onClick={() => setShowModal(false)}
className="ml-auto text-muted-foreground hover:text-foreground text-lg leading-none">×</button>
</div>
{/* Status: nicht konfiguriert */}
{sessionInfo && !sessionInfo.configured && (
<div className="space-y-3">
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<AlertTriangle size={16} className="text-yellow-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-yellow-300">{sessionInfo.message}</p>
</div>
<p className="text-sm text-muted-foreground">
MeshCentral läuft unter{' '}
<a href={sessionInfo.setupUrl} target="_blank" rel="noopener noreferrer"
className="text-primary hover:underline">{sessionInfo.setupUrl}</a>.
Richte MeshCentral ein und setze <code className="text-xs bg-muted px-1 rounded">MeshCentral:Enabled=true</code> in der appsettings.json.
</p>
<button onClick={() => window.open(sessionInfo.setupUrl, '_blank')}
className="flex items-center gap-2 w-full justify-center px-4 py-2 bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded-lg text-sm hover:bg-yellow-500/30">
<ExternalLink size={14} />
MeshCentral öffnen
</button>
</div>
)}
{/* Status: Agent nicht installiert */}
{sessionInfo?.configured && !sessionInfo.agentInstalled && (
<div className="space-y-3">
<div className="flex items-start gap-2 p-3 bg-orange-500/10 border border-orange-500/30 rounded-lg">
<AlertTriangle size={16} className="text-orange-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-orange-300">{sessionInfo.message}</p>
</div>
<p className="text-sm text-muted-foreground">
Der NexusRMM-Agent installiert MeshAgent automatisch wenn{' '}
<code className="text-xs bg-muted px-1 rounded">mesh_enabled: true</code> in der Agent-Config gesetzt ist.
</p>
{sessionInfo.meshAgentDownloadUrl && (
<button onClick={() => window.open(sessionInfo.meshAgentDownloadUrl, '_blank')}
className="flex items-center gap-2 w-full justify-center px-4 py-2 bg-muted border border-border rounded-lg text-sm hover:bg-accent">
<ExternalLink size={14} />
MeshAgent manuell herunterladen
</button>
)}
</div>
)}
{/* Status: bereit */}
{sessionInfo?.configured && sessionInfo.agentInstalled && (
<div className="space-y-3">
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
<p className="text-sm text-green-400 font-medium"> MeshAgent verbunden</p>
<p className="text-xs text-muted-foreground mt-1">Node ID: {sessionInfo.meshNodeId}</p>
</div>
<p className="text-sm text-muted-foreground">
Remote Desktop öffnet sich in einem neuen Fenster. Falls du nach einem Login gefragt wirst, melde dich bei MeshCentral an.
</p>
<button onClick={handleConnect}
className="flex items-center gap-2 w-full justify-center px-4 py-2 bg-purple-500/20 text-purple-400 border border-purple-500/30 rounded-lg text-sm hover:bg-purple-500/30">
<Monitor size={14} />
Remote Desktop öffnen
</button>
<button onClick={() => window.open(sessionInfo.meshCentralBaseUrl, '_blank')}
className="flex items-center gap-2 w-full justify-center px-4 py-2 bg-muted border border-border rounded-lg text-sm hover:bg-accent text-xs">
<ExternalLink size={12} />
MeshCentral Dashboard
</button>
</div>
)}
{error && (
<p className="text-sm text-red-400 mt-2">{error}</p>
)}
</div>
</div>
)}
</>
)
}

View File

@@ -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'}
</span>
<RemoteDesktopButton agentId={agentId} agentHostname={agent.hostname} className="ml-4" />
</div>
</div>
</div>