Initial commit
This commit is contained in:
88
electron-agent/README.md
Normal file
88
electron-agent/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# RemoteLink Agent
|
||||
|
||||
The RemoteLink Agent is an Electron-based desktop application that enables remote desktop control. It runs in the system tray and allows users to generate session codes for remote access.
|
||||
|
||||
## Features
|
||||
|
||||
- System tray integration with quick access menu
|
||||
- Session code generation (6-digit codes, 10-minute expiry)
|
||||
- Screen capture using Electron's desktopCapturer
|
||||
- WebRTC peer-to-peer connections
|
||||
- Multi-monitor support
|
||||
- Cross-platform (Windows, macOS, Linux)
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Navigate to the agent directory
|
||||
cd electron-agent
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build for all platforms
|
||||
npm run build
|
||||
npm run package
|
||||
|
||||
# Platform-specific builds
|
||||
npm run package:win # Windows
|
||||
npm run package:mac # macOS
|
||||
npm run package:linux # Linux
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
electron-agent/
|
||||
├── src/
|
||||
│ ├── main/ # Main process (Node.js)
|
||||
│ │ └── index.ts # Tray, IPC handlers, WebRTC
|
||||
│ ├── preload/ # Preload scripts (bridge)
|
||||
│ │ └── index.ts # Exposes safe APIs to renderer
|
||||
│ └── renderer/ # Renderer process (UI)
|
||||
│ └── index.html # Agent UI
|
||||
├── assets/ # Icons and images
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The agent needs to be configured with an access key from the RemoteLink web dashboard:
|
||||
|
||||
1. Register/login at the web dashboard
|
||||
2. Navigate to Machines > Add Machine
|
||||
3. Copy the generated access key
|
||||
4. Paste it in the agent's setup screen
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The agent communicates with these server endpoints:
|
||||
|
||||
- `POST /api/agent/register` - Register machine with server
|
||||
- `POST /api/agent/heartbeat` - Maintain online status
|
||||
- `POST /api/agent/session-code` - Generate session codes
|
||||
- `POST /api/signal` - WebRTC signaling
|
||||
|
||||
## Security
|
||||
|
||||
- Access keys are unique per machine
|
||||
- Session codes expire after 10 minutes
|
||||
- All WebRTC connections are encrypted
|
||||
- No screen data passes through servers (P2P)
|
||||
|
||||
## Input Simulation
|
||||
|
||||
For full remote control, the agent uses native libraries:
|
||||
|
||||
- **Windows**: `robotjs` or `nut.js`
|
||||
- **macOS**: Requires Accessibility permissions
|
||||
- **Linux**: X11 or Wayland protocols
|
||||
|
||||
Note: Input simulation is not implemented in this demo version.
|
||||
45
electron-agent/package.json
Normal file
45
electron-agent/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "remotelink-agent",
|
||||
"version": "1.0.0",
|
||||
"description": "RemoteLink Agent - Remote Desktop Control Agent",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
"preview": "electron-vite preview",
|
||||
"package": "electron-builder",
|
||||
"package:win": "electron-builder --win",
|
||||
"package:mac": "electron-builder --mac",
|
||||
"package:linux": "electron-builder --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"electron": "^29.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.remotelink.agent",
|
||||
"productName": "RemoteLink Agent",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"win": {
|
||||
"target": ["nsis"],
|
||||
"icon": "assets/icon.ico"
|
||||
},
|
||||
"mac": {
|
||||
"target": ["dmg"],
|
||||
"icon": "assets/icon.icns"
|
||||
},
|
||||
"linux": {
|
||||
"target": ["AppImage"],
|
||||
"icon": "assets/icon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
253
electron-agent/src/main/index.ts
Normal file
253
electron-agent/src/main/index.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* RemoteLink Agent - Main Process
|
||||
*
|
||||
* This is the main Electron process that:
|
||||
* - Creates the system tray icon
|
||||
* - Manages WebRTC connections
|
||||
* - Captures screen using desktopCapturer
|
||||
* - Handles input simulation
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, Tray, Menu, desktopCapturer, ipcMain, screen } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let tray: Tray | null = null
|
||||
|
||||
// Agent state
|
||||
interface AgentState {
|
||||
accessKey: string | null
|
||||
isConnected: boolean
|
||||
currentSessionCode: string | null
|
||||
sessionCodeExpiry: Date | null
|
||||
}
|
||||
|
||||
const agentState: AgentState = {
|
||||
accessKey: null,
|
||||
isConnected: false,
|
||||
currentSessionCode: null,
|
||||
sessionCodeExpiry: null,
|
||||
}
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'https://your-app.vercel.app'
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 500,
|
||||
resizable: false,
|
||||
minimizable: true,
|
||||
maximizable: false,
|
||||
show: false,
|
||||
frame: false,
|
||||
skipTaskbar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
event.preventDefault()
|
||||
mainWindow?.hide()
|
||||
})
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
const iconPath = path.join(__dirname, '../../assets/tray-icon.png')
|
||||
tray = new Tray(iconPath)
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Open RemoteLink',
|
||||
click: () => {
|
||||
mainWindow?.show()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Generate Session Code',
|
||||
click: async () => {
|
||||
await generateSessionCode()
|
||||
mainWindow?.show()
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Settings',
|
||||
click: () => {
|
||||
mainWindow?.show()
|
||||
mainWindow?.webContents.send('navigate', 'settings')
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
app.quit()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
tray.setToolTip('RemoteLink Agent')
|
||||
tray.setContextMenu(contextMenu)
|
||||
|
||||
tray.on('click', () => {
|
||||
mainWindow?.show()
|
||||
})
|
||||
}
|
||||
|
||||
// Generate session code via API
|
||||
async function generateSessionCode(): Promise<string | null> {
|
||||
if (!agentState.accessKey) {
|
||||
console.error('No access key configured')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agent/session-code`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accessKey: agentState.accessKey }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
agentState.currentSessionCode = data.code
|
||||
agentState.sessionCodeExpiry = new Date(data.expiresAt)
|
||||
|
||||
// Notify renderer
|
||||
mainWindow?.webContents.send('session-code-generated', {
|
||||
code: data.code,
|
||||
expiresAt: data.expiresAt,
|
||||
expiresIn: data.expiresIn,
|
||||
})
|
||||
|
||||
return data.code
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate session code:', error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Send heartbeat to server
|
||||
async function sendHeartbeat() {
|
||||
if (!agentState.accessKey) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agent/heartbeat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accessKey: agentState.accessKey }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.pendingConnection) {
|
||||
// Handle incoming connection request
|
||||
mainWindow?.webContents.send('incoming-connection', data.pendingConnection)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Heartbeat failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get available screen sources
|
||||
async function getScreenSources() {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 1920, height: 1080 },
|
||||
})
|
||||
|
||||
return sources.map(source => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL(),
|
||||
}))
|
||||
}
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('get-screen-sources', getScreenSources)
|
||||
|
||||
ipcMain.handle('generate-session-code', generateSessionCode)
|
||||
|
||||
ipcMain.handle('get-agent-state', () => ({
|
||||
isConnected: agentState.isConnected,
|
||||
currentSessionCode: agentState.currentSessionCode,
|
||||
sessionCodeExpiry: agentState.sessionCodeExpiry?.toISOString(),
|
||||
}))
|
||||
|
||||
ipcMain.handle('set-access-key', (_event, accessKey: string) => {
|
||||
agentState.accessKey = accessKey
|
||||
// Register with server
|
||||
registerAgent()
|
||||
})
|
||||
|
||||
ipcMain.handle('get-monitors', () => {
|
||||
return screen.getAllDisplays().map((display, index) => ({
|
||||
id: display.id,
|
||||
index,
|
||||
label: `Display ${index + 1}`,
|
||||
width: display.size.width,
|
||||
height: display.size.height,
|
||||
primary: display.id === screen.getPrimaryDisplay().id,
|
||||
}))
|
||||
})
|
||||
|
||||
// Register agent with server
|
||||
async function registerAgent() {
|
||||
if (!agentState.accessKey) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agent/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accessKey: agentState.accessKey,
|
||||
name: require('os').hostname(),
|
||||
hostname: require('os').hostname(),
|
||||
os: process.platform,
|
||||
osVersion: require('os').release(),
|
||||
agentVersion: app.getVersion(),
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
agentState.isConnected = true
|
||||
mainWindow?.webContents.send('registered', data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
createTray()
|
||||
|
||||
// Start heartbeat interval
|
||||
setInterval(sendHeartbeat, 30000) // Every 30 seconds
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Don't quit on window close - keep running in tray
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// Clean up
|
||||
tray?.destroy()
|
||||
})
|
||||
108
electron-agent/src/preload/index.ts
Normal file
108
electron-agent/src/preload/index.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* RemoteLink Agent - Preload Script
|
||||
* Exposes safe APIs to the renderer process
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
// Define types for exposed API
|
||||
interface AgentState {
|
||||
isConnected: boolean
|
||||
currentSessionCode: string | null
|
||||
sessionCodeExpiry: string | null
|
||||
}
|
||||
|
||||
interface ScreenSource {
|
||||
id: string
|
||||
name: string
|
||||
thumbnail: string
|
||||
}
|
||||
|
||||
interface Monitor {
|
||||
id: number
|
||||
index: number
|
||||
label: string
|
||||
width: number
|
||||
height: number
|
||||
primary: boolean
|
||||
}
|
||||
|
||||
interface SessionCodeEvent {
|
||||
code: string
|
||||
expiresAt: string
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
// Expose APIs to renderer
|
||||
contextBridge.exposeInMainWorld('remotelink', {
|
||||
// Get current agent state
|
||||
getAgentState: (): Promise<AgentState> => {
|
||||
return ipcRenderer.invoke('get-agent-state')
|
||||
},
|
||||
|
||||
// Set access key for registration
|
||||
setAccessKey: (accessKey: string): Promise<void> => {
|
||||
return ipcRenderer.invoke('set-access-key', accessKey)
|
||||
},
|
||||
|
||||
// Generate a new session code
|
||||
generateSessionCode: (): Promise<string | null> => {
|
||||
return ipcRenderer.invoke('generate-session-code')
|
||||
},
|
||||
|
||||
// Get available screen sources for sharing
|
||||
getScreenSources: (): Promise<ScreenSource[]> => {
|
||||
return ipcRenderer.invoke('get-screen-sources')
|
||||
},
|
||||
|
||||
// Get connected monitors
|
||||
getMonitors: (): Promise<Monitor[]> => {
|
||||
return ipcRenderer.invoke('get-monitors')
|
||||
},
|
||||
|
||||
// Event listeners
|
||||
onSessionCodeGenerated: (callback: (event: SessionCodeEvent) => void) => {
|
||||
ipcRenderer.on('session-code-generated', (_event, data) => callback(data))
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners('session-code-generated')
|
||||
}
|
||||
},
|
||||
|
||||
onIncomingConnection: (callback: (data: { sessionCodeId: string; usedBy: string }) => void) => {
|
||||
ipcRenderer.on('incoming-connection', (_event, data) => callback(data))
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners('incoming-connection')
|
||||
}
|
||||
},
|
||||
|
||||
onRegistered: (callback: (data: { success: boolean; machineId: string }) => void) => {
|
||||
ipcRenderer.on('registered', (_event, data) => callback(data))
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners('registered')
|
||||
}
|
||||
},
|
||||
|
||||
onNavigate: (callback: (page: string) => void) => {
|
||||
ipcRenderer.on('navigate', (_event, page) => callback(page))
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners('navigate')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Type declaration for window object
|
||||
declare global {
|
||||
interface Window {
|
||||
remotelink: {
|
||||
getAgentState: () => Promise<AgentState>
|
||||
setAccessKey: (accessKey: string) => Promise<void>
|
||||
generateSessionCode: () => Promise<string | null>
|
||||
getScreenSources: () => Promise<ScreenSource[]>
|
||||
getMonitors: () => Promise<Monitor[]>
|
||||
onSessionCodeGenerated: (callback: (event: SessionCodeEvent) => void) => () => void
|
||||
onIncomingConnection: (callback: (data: { sessionCodeId: string; usedBy: string }) => void) => () => void
|
||||
onRegistered: (callback: (data: { success: boolean; machineId: string }) => void) => () => void
|
||||
onNavigate: (callback: (page: string) => void) => () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
350
electron-agent/src/renderer/index.html
Normal file
350
electron-agent/src/renderer/index.html
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RemoteLink Agent</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f0f17;
|
||||
color: #e4e4e7;
|
||||
min-height: 100vh;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #1a1a24;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.status-dot.offline {
|
||||
background: #71717a;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.session-code {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
background: #1f1f2a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.session-code-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #71717a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.session-code-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.session-code-expiry {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
-webkit-app-region: no-drag;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.generate-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.setup-card {
|
||||
background: #1a1a24;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input {
|
||||
-webkit-app-region: no-drag;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #0f0f17;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 6px;
|
||||
color: #e4e4e7;
|
||||
font-size: 14px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #52525b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="title">RemoteLink Agent</span>
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span class="status-text" id="statusText">Connecting...</span>
|
||||
</div>
|
||||
|
||||
<div class="session-code" id="sessionCodeContainer" style="display: none;">
|
||||
<div class="session-code-label">Session Code</div>
|
||||
<div class="session-code-value" id="sessionCodeValue">---</div>
|
||||
<div class="session-code-expiry" id="sessionCodeExpiry">Valid for 10 minutes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="generate-btn" id="generateBtn">Generate Session Code</button>
|
||||
|
||||
<div class="setup-card" id="setupCard" style="margin-top: 20px; display: none;">
|
||||
<div class="setup-title">Setup Required</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">Access Key</label>
|
||||
<input type="text" class="input" id="accessKeyInput" placeholder="Paste your access key here">
|
||||
</div>
|
||||
<button class="generate-btn" id="saveKeyBtn">Save & Connect</button>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
RemoteLink Agent v1.0.0<br>
|
||||
Secure remote desktop solution
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// UI Elements
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const sessionCodeContainer = document.getElementById('sessionCodeContainer');
|
||||
const sessionCodeValue = document.getElementById('sessionCodeValue');
|
||||
const sessionCodeExpiry = document.getElementById('sessionCodeExpiry');
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
const setupCard = document.getElementById('setupCard');
|
||||
const accessKeyInput = document.getElementById('accessKeyInput');
|
||||
const saveKeyBtn = document.getElementById('saveKeyBtn');
|
||||
|
||||
// State
|
||||
let countdownInterval = null;
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
const state = await window.remotelink.getAgentState();
|
||||
|
||||
if (state.isConnected) {
|
||||
setConnected();
|
||||
if (state.currentSessionCode && state.sessionCodeExpiry) {
|
||||
showSessionCode(state.currentSessionCode, state.sessionCodeExpiry);
|
||||
}
|
||||
} else {
|
||||
setDisconnected();
|
||||
setupCard.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function setConnected() {
|
||||
statusDot.classList.remove('offline');
|
||||
statusText.textContent = 'Connected to server';
|
||||
setupCard.style.display = 'none';
|
||||
}
|
||||
|
||||
function setDisconnected() {
|
||||
statusDot.classList.add('offline');
|
||||
statusText.textContent = 'Not configured';
|
||||
}
|
||||
|
||||
function showSessionCode(code, expiresAt) {
|
||||
sessionCodeContainer.style.display = 'block';
|
||||
sessionCodeValue.textContent = code.slice(0, 3) + ' ' + code.slice(3);
|
||||
|
||||
// Start countdown
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
|
||||
function updateCountdown() {
|
||||
const now = new Date();
|
||||
const expiry = new Date(expiresAt);
|
||||
const remaining = Math.max(0, Math.floor((expiry - now) / 1000));
|
||||
|
||||
if (remaining <= 0) {
|
||||
sessionCodeExpiry.textContent = 'Code expired';
|
||||
sessionCodeContainer.style.display = 'none';
|
||||
clearInterval(countdownInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(remaining / 60);
|
||||
const seconds = remaining % 60;
|
||||
sessionCodeExpiry.textContent = `Expires in ${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
countdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
generateBtn.addEventListener('click', async () => {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.textContent = 'Generating...';
|
||||
|
||||
const code = await window.remotelink.generateSessionCode();
|
||||
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.textContent = 'Generate Session Code';
|
||||
|
||||
if (!code) {
|
||||
alert('Failed to generate code. Please check your connection.');
|
||||
}
|
||||
});
|
||||
|
||||
saveKeyBtn.addEventListener('click', async () => {
|
||||
const accessKey = accessKeyInput.value.trim();
|
||||
if (!accessKey) return;
|
||||
|
||||
saveKeyBtn.disabled = true;
|
||||
saveKeyBtn.textContent = 'Connecting...';
|
||||
|
||||
await window.remotelink.setAccessKey(accessKey);
|
||||
});
|
||||
|
||||
// IPC event listeners
|
||||
window.remotelink.onSessionCodeGenerated((event) => {
|
||||
showSessionCode(event.code, event.expiresAt);
|
||||
});
|
||||
|
||||
window.remotelink.onRegistered((data) => {
|
||||
if (data.success) {
|
||||
setConnected();
|
||||
}
|
||||
saveKeyBtn.disabled = false;
|
||||
saveKeyBtn.textContent = 'Save & Connect';
|
||||
});
|
||||
|
||||
window.remotelink.onIncomingConnection((data) => {
|
||||
// Show notification that someone is connecting
|
||||
new Notification('RemoteLink', {
|
||||
body: 'Incoming connection request',
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize on load
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user