Add ScreenConnect-parity features (high + medium)
Viewer: - Toolbar: Ctrl+Alt+Del, clipboard paste, monitor picker, file transfer, chat, WoL buttons - Multi-monitor: agent sends monitor_list on connect, viewer can switch via dropdown - Clipboard sync: agent polls local clipboard → sends to viewer; viewer paste → agent sets remote clipboard - File transfer panel: drag-drop upload to agent, directory browser, download files from remote - Chat panel: bidirectional text chat forwarded through relay Agent: - Multi-monitor capture with set_monitor/set_quality message handlers - exec_key_combo for Ctrl+Alt+Del and arbitrary combos - Clipboard polling via pyperclip (both directions) - File upload/download/list_files with base64 chunked protocol - Attended mode (--attended): zenity/kdialog/PowerShell consent dialog before accepting stream - Auto-update: heartbeat checks version, downloads new binary and exec-replaces self (Linux) - Reports MAC address on registration (for WoL) Relay: - Forwards monitor_list, clipboard_content, file_chunk, file_list, chat_message agent→viewer - Session recording: when RECORDING_DIR env set, saves JPEG frames as .remrec files - ALLOWED_ORIGINS CORS now set from NEXT_PUBLIC_APP_URL in docker-compose Database: - groups table (id, name, description, created_by) - machines: group_id, mac_address, notes, tags text[] - Migration 0003 applied Dashboard: - Machines page: search, tag filter, group filter, inline notes/tags/rename editing - MachineCard: inline tag management, group picker, notes textarea - Admin page: new Groups tab (create/list/delete groups) - API: PATCH /api/machines/[id] (name, notes, tags, groupId) - API: GET/POST/DELETE /api/groups - API: POST /api/machines/wol (broadcast magic packet) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,32 +9,41 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Monitor,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Settings,
|
||||
X,
|
||||
RefreshCw,
|
||||
import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Monitor,
|
||||
Signal,
|
||||
SignalLow,
|
||||
SignalMedium,
|
||||
Keyboard,
|
||||
MousePointer2,
|
||||
Loader2,
|
||||
CheckCircle2
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
X,
|
||||
Clipboard,
|
||||
FolderOpen,
|
||||
ChevronDown,
|
||||
MessageSquare,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface MonitorInfo {
|
||||
index: number
|
||||
width: number
|
||||
height: number
|
||||
primary: boolean
|
||||
}
|
||||
|
||||
interface Session {
|
||||
id: string
|
||||
machine_id: string | null
|
||||
machine_name: string | null
|
||||
started_at: string
|
||||
machineId: string | null
|
||||
machineName: string | null
|
||||
startedAt: string
|
||||
}
|
||||
|
||||
type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error'
|
||||
type ConnectionState = 'connecting' | 'waiting' | 'connected' | 'disconnected' | 'error'
|
||||
|
||||
interface ViewerToolbarProps {
|
||||
session: Session | null
|
||||
@@ -42,11 +51,20 @@ interface ViewerToolbarProps {
|
||||
isFullscreen: boolean
|
||||
quality: 'high' | 'medium' | 'low'
|
||||
isMuted: boolean
|
||||
monitors: MonitorInfo[]
|
||||
activeMonitor: number
|
||||
onToggleFullscreen: () => void
|
||||
onQualityChange: (quality: 'high' | 'medium' | 'low') => void
|
||||
onToggleMute: () => void
|
||||
onDisconnect: () => void
|
||||
onReconnect: () => void
|
||||
onCtrlAltDel: () => void
|
||||
onClipboardPaste: () => void
|
||||
onMonitorChange: (index: number) => void
|
||||
onToggleFileTransfer: () => void
|
||||
onToggleChat: () => void
|
||||
onWakeOnLan?: () => void
|
||||
machineName?: string
|
||||
}
|
||||
|
||||
export function ViewerToolbar({
|
||||
@@ -54,13 +72,21 @@ export function ViewerToolbar({
|
||||
connectionState,
|
||||
isFullscreen,
|
||||
quality,
|
||||
isMuted,
|
||||
monitors,
|
||||
activeMonitor,
|
||||
onToggleFullscreen,
|
||||
onQualityChange,
|
||||
onToggleMute,
|
||||
onDisconnect,
|
||||
onReconnect,
|
||||
onCtrlAltDel,
|
||||
onClipboardPaste,
|
||||
onMonitorChange,
|
||||
onToggleFileTransfer,
|
||||
onToggleChat,
|
||||
onWakeOnLan,
|
||||
}: ViewerToolbarProps) {
|
||||
const connected = connectionState === 'connected'
|
||||
|
||||
const getQualityIcon = () => {
|
||||
switch (quality) {
|
||||
case 'high': return Signal
|
||||
@@ -73,28 +99,35 @@ export function ViewerToolbar({
|
||||
switch (connectionState) {
|
||||
case 'connecting':
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-yellow-500">
|
||||
<span className="flex items-center gap-1.5 text-yellow-500 text-xs">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Connecting
|
||||
</span>
|
||||
)
|
||||
case 'waiting':
|
||||
return (
|
||||
<span className="flex items-center gap-1.5 text-yellow-500 text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-yellow-500 animate-pulse" />
|
||||
Waiting for agent
|
||||
</span>
|
||||
)
|
||||
case 'connected':
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-green-500">
|
||||
<span className="flex items-center gap-1.5 text-green-500 text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500" />
|
||||
Connected
|
||||
</span>
|
||||
)
|
||||
case 'disconnected':
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5 text-muted-foreground text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-muted-foreground" />
|
||||
Disconnected
|
||||
</span>
|
||||
)
|
||||
case 'error':
|
||||
return (
|
||||
<span className="flex items-center gap-2 text-destructive">
|
||||
<span className="flex items-center gap-1.5 text-destructive text-xs">
|
||||
<span className="h-2 w-2 rounded-full bg-destructive" />
|
||||
Error
|
||||
</span>
|
||||
@@ -105,32 +138,108 @@ export function ViewerToolbar({
|
||||
const QualityIcon = getQualityIcon()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between h-12 px-4 bg-card/95 backdrop-blur border-b border-border/50">
|
||||
{/* Left section */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="h-4 w-4 text-primary" />
|
||||
<span className="font-medium text-sm">
|
||||
{session?.machine_name || 'Remote Machine'}
|
||||
<div className="flex items-center justify-between h-12 px-4 bg-card/95 backdrop-blur border-b border-border/50 gap-2">
|
||||
{/* Left: machine name + status */}
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Monitor className="h-4 w-4 text-primary shrink-0" />
|
||||
<span className="font-medium text-sm truncate">
|
||||
{session?.machineName || 'Remote Machine'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{getConnectionBadge()}
|
||||
</div>
|
||||
<div className="shrink-0">{getConnectionBadge()}</div>
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Right: controls */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{/* Input indicators */}
|
||||
<div className="flex items-center gap-2 px-3 border-r border-border/50 mr-2">
|
||||
<MousePointer2 className="h-4 w-4 text-muted-foreground" />
|
||||
<Keyboard className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex items-center gap-1.5 px-2 border-r border-border/50 mr-1">
|
||||
<MousePointer2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Keyboard className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
{/* Monitor picker (only shown when multiple monitors) */}
|
||||
{connected && monitors.length > 1 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 gap-1 px-2 text-xs">
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
{monitors[activeMonitor]
|
||||
? `${monitors[activeMonitor].width}×${monitors[activeMonitor].height}`
|
||||
: 'Monitor'}
|
||||
<ChevronDown className="h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Select Monitor</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{monitors.map((m) => (
|
||||
<DropdownMenuItem key={m.index} onClick={() => onMonitorChange(m.index)}>
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
{m.primary ? 'Primary' : `Display ${m.index + 1}`}
|
||||
<span className="ml-auto text-xs text-muted-foreground">{m.width}×{m.height}</span>
|
||||
{activeMonitor === m.index && <CheckCircle2 className="ml-2 h-4 w-4 text-primary" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Ctrl+Alt+Del */}
|
||||
{connected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs font-mono"
|
||||
onClick={onCtrlAltDel}
|
||||
title="Send Ctrl+Alt+Del"
|
||||
>
|
||||
CAD
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Clipboard paste */}
|
||||
{connected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={onClipboardPaste}
|
||||
title="Paste clipboard to remote"
|
||||
>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
<span className="sr-only">Paste clipboard</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* File transfer */}
|
||||
{connected && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onToggleFileTransfer} title="File transfer">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="sr-only">File transfer</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Chat */}
|
||||
{connected && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onToggleChat} title="Session chat">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span className="sr-only">Chat</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Wake-on-LAN (shown when agent is offline) */}
|
||||
{!connected && onWakeOnLan && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onWakeOnLan} title="Wake on LAN">
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
<span className="sr-only">Wake on LAN</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Quality selector */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" title="Stream quality">
|
||||
<QualityIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Quality</span>
|
||||
</Button>
|
||||
@@ -156,43 +265,27 @@ export function ViewerToolbar({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Mute toggle */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onToggleMute}>
|
||||
{isMuted ? (
|
||||
<VolumeX className="h-4 w-4" />
|
||||
) : (
|
||||
<Volume2 className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">{isMuted ? 'Unmute' : 'Mute'}</span>
|
||||
</Button>
|
||||
|
||||
{/* Fullscreen toggle */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onToggleFullscreen}>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">{isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}</span>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onToggleFullscreen} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
|
||||
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{/* Reconnect */}
|
||||
{connectionState === 'disconnected' && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onReconnect}>
|
||||
{(connectionState === 'disconnected' || connectionState === 'waiting') && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onReconnect} title="Reconnect">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<span className="sr-only">Reconnect</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Disconnect */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={onDisconnect}
|
||||
title="End session"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Disconnect</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user