Link support codes to agent sessions
- Server: Accept support_code param in WebSocket connection - Server: Link code to session when agent connects, mark as connected - Server: Mark code as completed when agent disconnects - Agent: Accept support code from command line argument - Agent: Send hostname and support_code in WebSocket params - Portal: Trigger agent download with code in filename - Portal: Show code reminder in download instructions - Dashboard: Add machines list fetching (Access tab) - Add TODO.md for feature tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
230
TODO.md
Normal file
230
TODO.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# GuruConnect Feature Tracking
|
||||||
|
|
||||||
|
## Status Legend
|
||||||
|
- [ ] Not started
|
||||||
|
- [~] In progress
|
||||||
|
- [x] Complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Core MVP
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- [x] WebSocket relay server (Axum)
|
||||||
|
- [x] Agent WebSocket client
|
||||||
|
- [x] Protobuf message protocol
|
||||||
|
- [x] Agent authentication (agent_id, api_key)
|
||||||
|
- [x] Session management (create, join, leave)
|
||||||
|
- [x] Systemd service deployment
|
||||||
|
- [x] NPM proxy (connect.azcomputerguru.com)
|
||||||
|
|
||||||
|
### Support Codes
|
||||||
|
- [x] Generate 6-digit codes
|
||||||
|
- [x] Code validation API
|
||||||
|
- [x] Code status tracking (pending, connected, completed, cancelled)
|
||||||
|
- [~] Link support codes to agent sessions
|
||||||
|
- [ ] Code expiration (auto-expire after X minutes)
|
||||||
|
- [ ] Support code in agent download URL
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
- [x] Technician login page
|
||||||
|
- [x] Support tab with code generation
|
||||||
|
- [x] Access tab with connected agents
|
||||||
|
- [ ] Session detail panel with tabs
|
||||||
|
- [ ] Screenshot thumbnails
|
||||||
|
- [ ] Join/Connect button
|
||||||
|
|
||||||
|
### Agent (Windows)
|
||||||
|
- [x] DXGI screen capture
|
||||||
|
- [x] GDI fallback capture
|
||||||
|
- [x] WebSocket connection
|
||||||
|
- [x] Config persistence (agent_id)
|
||||||
|
- [ ] Support code parameter
|
||||||
|
- [ ] Hostname/machine info reporting
|
||||||
|
- [ ] Screenshot-only mode (for thumbnails)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Remote Control
|
||||||
|
|
||||||
|
### Screen Viewing
|
||||||
|
- [ ] Web-based viewer (canvas)
|
||||||
|
- [ ] Raw frame decoding
|
||||||
|
- [ ] Dirty rectangle optimization
|
||||||
|
- [ ] Frame rate adaptation
|
||||||
|
|
||||||
|
### Input Control
|
||||||
|
- [x] Mouse event handling (agent)
|
||||||
|
- [x] Keyboard event handling (agent)
|
||||||
|
- [ ] Input relay through server
|
||||||
|
- [ ] Multi-monitor support
|
||||||
|
|
||||||
|
### Encoding
|
||||||
|
- [ ] VP9 software encoding
|
||||||
|
- [ ] H.264 hardware encoding (NVENC/QSV)
|
||||||
|
- [ ] Adaptive quality based on bandwidth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Backstage Tools (like ScreenConnect)
|
||||||
|
|
||||||
|
### Device Information
|
||||||
|
- [ ] OS version, hostname, domain
|
||||||
|
- [ ] Logged-in user
|
||||||
|
- [ ] Public/private IP addresses
|
||||||
|
- [ ] MAC address
|
||||||
|
- [ ] CPU, RAM, disk info
|
||||||
|
- [ ] Uptime
|
||||||
|
|
||||||
|
### Toolbox APIs
|
||||||
|
- [ ] Process list (name, PID, memory)
|
||||||
|
- [ ] Installed software list
|
||||||
|
- [ ] Windows services list
|
||||||
|
- [ ] Event log viewer
|
||||||
|
- [ ] Registry browser
|
||||||
|
|
||||||
|
### Remote Commands
|
||||||
|
- [ ] Run shell commands
|
||||||
|
- [ ] PowerShell execution
|
||||||
|
- [ ] Command output streaming
|
||||||
|
- [ ] Command history per session
|
||||||
|
|
||||||
|
### Chat/Messaging
|
||||||
|
- [ ] Technician → Client messages
|
||||||
|
- [ ] Client → Technician messages
|
||||||
|
- [ ] Message history
|
||||||
|
|
||||||
|
### File Transfer
|
||||||
|
- [ ] Upload files to remote
|
||||||
|
- [ ] Download files from remote
|
||||||
|
- [ ] Progress tracking
|
||||||
|
- [ ] Folder browsing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Session Management
|
||||||
|
|
||||||
|
### Timeline/History
|
||||||
|
- [ ] Connection events
|
||||||
|
- [ ] Session duration tracking
|
||||||
|
- [ ] Guest connection history
|
||||||
|
- [ ] Activity log
|
||||||
|
|
||||||
|
### Session Recording
|
||||||
|
- [ ] Record session video
|
||||||
|
- [ ] Playback interface
|
||||||
|
- [ ] Storage management
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- [ ] Per-session notes
|
||||||
|
- [ ] Session tagging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Access Mode (Unattended)
|
||||||
|
|
||||||
|
### Persistent Agent
|
||||||
|
- [ ] Windows service installation
|
||||||
|
- [ ] Auto-start on boot
|
||||||
|
- [ ] Silent/background mode
|
||||||
|
- [ ] Automatic reconnection
|
||||||
|
|
||||||
|
### Machine Groups
|
||||||
|
- [ ] Company/client organization
|
||||||
|
- [ ] Site/location grouping
|
||||||
|
- [ ] Custom tags
|
||||||
|
- [ ] Filtering/search
|
||||||
|
|
||||||
|
### Installer Builder
|
||||||
|
- [ ] Customized agent builds
|
||||||
|
- [ ] Pre-configured company/site
|
||||||
|
- [ ] Silent install options
|
||||||
|
- [ ] MSI packaging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Security & Authentication
|
||||||
|
|
||||||
|
### Technician Auth
|
||||||
|
- [ ] User accounts
|
||||||
|
- [ ] Password hashing
|
||||||
|
- [ ] JWT tokens
|
||||||
|
- [ ] Session management
|
||||||
|
|
||||||
|
### MFA
|
||||||
|
- [ ] TOTP (Google Authenticator)
|
||||||
|
- [ ] Email verification
|
||||||
|
|
||||||
|
### Audit Logging
|
||||||
|
- [ ] Login attempts
|
||||||
|
- [ ] Session access
|
||||||
|
- [ ] Command execution
|
||||||
|
- [ ] File transfers
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- [ ] Role-based access
|
||||||
|
- [ ] Per-client permissions
|
||||||
|
- [ ] Feature restrictions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Integrations
|
||||||
|
|
||||||
|
### PSA Integration
|
||||||
|
- [ ] HaloPSA
|
||||||
|
- [ ] Autotask
|
||||||
|
- [ ] ConnectWise
|
||||||
|
|
||||||
|
### GuruRMM Integration
|
||||||
|
- [ ] Dashboard embedding
|
||||||
|
- [ ] Single sign-on
|
||||||
|
- [ ] Asset linking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Polish
|
||||||
|
|
||||||
|
### Branding
|
||||||
|
- [ ] White-label support
|
||||||
|
- [ ] Custom logos
|
||||||
|
- [ ] Custom colors
|
||||||
|
|
||||||
|
### Mobile Support
|
||||||
|
- [ ] Responsive viewer
|
||||||
|
- [ ] Touch input handling
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
- [ ] Draw on screen
|
||||||
|
- [ ] Pointer highlighting
|
||||||
|
- [ ] Screenshot annotations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Sprint
|
||||||
|
|
||||||
|
### In Progress
|
||||||
|
1. Link support codes to agent sessions
|
||||||
|
2. Show connected status in dashboard
|
||||||
|
|
||||||
|
### Next Up
|
||||||
|
1. Support code in agent download/config
|
||||||
|
2. Device info reporting from agent
|
||||||
|
3. Screenshot thumbnails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### ScreenConnect Feature Reference (from screenshots)
|
||||||
|
- Support session list with idle times and connection bars
|
||||||
|
- Detail panel with tabbed interface:
|
||||||
|
- Join/Screen (thumbnail, Join button)
|
||||||
|
- Info (device details)
|
||||||
|
- Timeline (connection history)
|
||||||
|
- Chat (messaging)
|
||||||
|
- Commands (shell execution)
|
||||||
|
- Notes
|
||||||
|
- Toolbox (processes, software, events, services)
|
||||||
|
- File transfer
|
||||||
|
- Logs
|
||||||
|
- Settings
|
||||||
@@ -48,6 +48,9 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
# Hostname
|
# Hostname
|
||||||
hostname = "0.4"
|
hostname = "0.4"
|
||||||
|
|
||||||
|
# URL encoding
|
||||||
|
urlencoding = "2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
# Windows APIs for screen capture and input
|
# Windows APIs for screen capture and input
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ pub struct Config {
|
|||||||
/// Optional hostname override
|
/// Optional hostname override
|
||||||
pub hostname_override: Option<String>,
|
pub hostname_override: Option<String>,
|
||||||
|
|
||||||
|
/// Support code for one-time support sessions (set via command line)
|
||||||
|
#[serde(skip)]
|
||||||
|
pub support_code: Option<String>,
|
||||||
|
|
||||||
/// Capture settings
|
/// Capture settings
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub capture: CaptureConfig,
|
pub capture: CaptureConfig,
|
||||||
@@ -119,6 +123,9 @@ impl Config {
|
|||||||
let _ = config.save();
|
let _ = config.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// support_code is always None when loading from file (set via CLI)
|
||||||
|
config.support_code = None;
|
||||||
|
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +144,7 @@ impl Config {
|
|||||||
api_key,
|
api_key,
|
||||||
agent_id,
|
agent_id,
|
||||||
hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
|
hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
|
||||||
|
support_code: None, // Set via CLI
|
||||||
capture: CaptureConfig::default(),
|
capture: CaptureConfig::default(),
|
||||||
encoding: EncodingConfig::default(),
|
encoding: EncodingConfig::default(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
//! GuruConnect Agent - Remote Desktop Agent for Windows
|
//! GuruConnect Agent - Remote Desktop Agent for Windows
|
||||||
//!
|
//!
|
||||||
//! Provides screen capture, input injection, and remote control capabilities.
|
//! Provides screen capture, input injection, and remote control capabilities.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! guruconnect-agent.exe [support_code]
|
||||||
|
//!
|
||||||
|
//! If a support code is provided, the agent will connect using that code
|
||||||
|
//! for a one-time support session.
|
||||||
|
|
||||||
mod capture;
|
mod capture;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -28,8 +34,25 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
info!("GuruConnect Agent v{}", env!("CARGO_PKG_VERSION"));
|
info!("GuruConnect Agent v{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
let support_code = if args.len() > 1 {
|
||||||
|
let code = args[1].trim().to_string();
|
||||||
|
// Validate it looks like a 6-digit code
|
||||||
|
if code.len() == 6 && code.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
info!("Support code provided: {}", code);
|
||||||
|
Some(code)
|
||||||
|
} else {
|
||||||
|
info!("Invalid support code format, ignoring: {}", code);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
let config = config::Config::load()?;
|
let mut config = config::Config::load()?;
|
||||||
|
config.support_code = support_code;
|
||||||
info!("Loaded configuration for server: {}", config.server_url);
|
info!("Loaded configuration for server: {}", config.server_url);
|
||||||
|
|
||||||
// Run the agent
|
// Run the agent
|
||||||
|
|||||||
@@ -46,10 +46,13 @@ impl SessionManager {
|
|||||||
pub async fn connect(&mut self) -> Result<()> {
|
pub async fn connect(&mut self) -> Result<()> {
|
||||||
self.state = SessionState::Connecting;
|
self.state = SessionState::Connecting;
|
||||||
|
|
||||||
|
let hostname = self.config.hostname();
|
||||||
let transport = WebSocketTransport::connect(
|
let transport = WebSocketTransport::connect(
|
||||||
&self.config.server_url,
|
&self.config.server_url,
|
||||||
&self.config.agent_id,
|
&self.config.agent_id,
|
||||||
&self.config.api_key,
|
&self.config.api_key,
|
||||||
|
Some(&hostname),
|
||||||
|
self.config.support_code.as_deref(),
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
self.transport = Some(transport);
|
self.transport = Some(transport);
|
||||||
|
|||||||
@@ -29,15 +29,35 @@ pub struct WebSocketTransport {
|
|||||||
|
|
||||||
impl WebSocketTransport {
|
impl WebSocketTransport {
|
||||||
/// Connect to the server
|
/// Connect to the server
|
||||||
pub async fn connect(url: &str, agent_id: &str, api_key: &str) -> Result<Self> {
|
pub async fn connect(
|
||||||
// Append agent_id and API key as query parameters
|
url: &str,
|
||||||
|
agent_id: &str,
|
||||||
|
api_key: &str,
|
||||||
|
hostname: Option<&str>,
|
||||||
|
support_code: Option<&str>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Build query parameters
|
||||||
|
let mut params = format!("agent_id={}&api_key={}", agent_id, api_key);
|
||||||
|
|
||||||
|
if let Some(hostname) = hostname {
|
||||||
|
params.push_str(&format!("&hostname={}", urlencoding::encode(hostname)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(code) = support_code {
|
||||||
|
params.push_str(&format!("&support_code={}", code));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append parameters to URL
|
||||||
let url_with_params = if url.contains('?') {
|
let url_with_params = if url.contains('?') {
|
||||||
format!("{}&agent_id={}&api_key={}", url, agent_id, api_key)
|
format!("{}&{}", url, params)
|
||||||
} else {
|
} else {
|
||||||
format!("{}?agent_id={}&api_key={}", url, agent_id, api_key)
|
format!("{}?{}", url, params)
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("Connecting to {} as agent {}", url, agent_id);
|
tracing::info!("Connecting to {} as agent {}", url, agent_id);
|
||||||
|
if let Some(code) = support_code {
|
||||||
|
tracing::info!("Using support code: {}", code);
|
||||||
|
}
|
||||||
|
|
||||||
let (ws_stream, response) = connect_async(&url_with_params)
|
let (ws_stream, response) = connect_async(&url_with_params)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ pub struct AgentParams {
|
|||||||
agent_id: String,
|
agent_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
agent_name: Option<String>,
|
agent_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
support_code: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
hostname: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -38,10 +42,12 @@ pub async fn agent_ws_handler(
|
|||||||
Query(params): Query<AgentParams>,
|
Query(params): Query<AgentParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let agent_id = params.agent_id;
|
let agent_id = params.agent_id;
|
||||||
let agent_name = params.agent_name.unwrap_or_else(|| agent_id.clone());
|
let agent_name = params.hostname.or(params.agent_name).unwrap_or_else(|| agent_id.clone());
|
||||||
|
let support_code = params.support_code;
|
||||||
let sessions = state.sessions.clone();
|
let sessions = state.sessions.clone();
|
||||||
|
let support_codes = state.support_codes.clone();
|
||||||
|
|
||||||
ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, agent_id, agent_name))
|
ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, agent_id, agent_name, support_code))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket handler for viewer connections
|
/// WebSocket handler for viewer connections
|
||||||
@@ -60,8 +66,10 @@ pub async fn viewer_ws_handler(
|
|||||||
async fn handle_agent_connection(
|
async fn handle_agent_connection(
|
||||||
socket: WebSocket,
|
socket: WebSocket,
|
||||||
sessions: SessionManager,
|
sessions: SessionManager,
|
||||||
|
support_codes: crate::support_codes::SupportCodeManager,
|
||||||
agent_id: String,
|
agent_id: String,
|
||||||
agent_name: String,
|
agent_name: String,
|
||||||
|
support_code: Option<String>,
|
||||||
) {
|
) {
|
||||||
info!("Agent connected: {} ({})", agent_name, agent_id);
|
info!("Agent connected: {} ({})", agent_name, agent_id);
|
||||||
|
|
||||||
@@ -70,6 +78,13 @@ async fn handle_agent_connection(
|
|||||||
|
|
||||||
info!("Session created: {}", session_id);
|
info!("Session created: {}", session_id);
|
||||||
|
|
||||||
|
// If a support code was provided, mark it as connected
|
||||||
|
if let Some(ref code) = support_code {
|
||||||
|
info!("Linking support code {} to session {}", code, session_id);
|
||||||
|
support_codes.mark_connected(code, Some(agent_name.clone()), Some(agent_id.clone())).await;
|
||||||
|
support_codes.link_session(code, session_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
let (mut ws_sender, mut ws_receiver) = socket.split();
|
let (mut ws_sender, mut ws_receiver) = socket.split();
|
||||||
|
|
||||||
// Task to forward input events from viewers to agent
|
// Task to forward input events from viewers to agent
|
||||||
@@ -82,6 +97,8 @@ async fn handle_agent_connection(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let sessions_cleanup = sessions.clone();
|
let sessions_cleanup = sessions.clone();
|
||||||
|
let support_codes_cleanup = support_codes.clone();
|
||||||
|
let support_code_cleanup = support_code.clone();
|
||||||
|
|
||||||
// Main loop: receive frames from agent and broadcast to viewers
|
// Main loop: receive frames from agent and broadcast to viewers
|
||||||
while let Some(msg) = ws_receiver.next().await {
|
while let Some(msg) = ws_receiver.next().await {
|
||||||
@@ -119,6 +136,13 @@ async fn handle_agent_connection(
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
input_forward.abort();
|
input_forward.abort();
|
||||||
sessions_cleanup.remove_session(session_id).await;
|
sessions_cleanup.remove_session(session_id).await;
|
||||||
|
|
||||||
|
// Mark support code as completed if one was used
|
||||||
|
if let Some(ref code) = support_code_cleanup {
|
||||||
|
support_codes_cleanup.mark_completed(code).await;
|
||||||
|
info!("Support code {} marked as completed", code);
|
||||||
|
}
|
||||||
|
|
||||||
info!("Session {} ended", session_id);
|
info!("Session {} ended", session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,27 @@ impl SupportCodeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Link a support code to an actual WebSocket session
|
||||||
|
pub async fn link_session(&self, code: &str, real_session_id: Uuid) {
|
||||||
|
let mut codes = self.codes.write().await;
|
||||||
|
if let Some(support_code) = codes.get_mut(code) {
|
||||||
|
// Update session_to_code mapping with real session ID
|
||||||
|
let old_session_id = support_code.session_id;
|
||||||
|
support_code.session_id = real_session_id;
|
||||||
|
|
||||||
|
// Update the reverse mapping
|
||||||
|
let mut session_to_code = self.session_to_code.write().await;
|
||||||
|
session_to_code.remove(&old_session_id);
|
||||||
|
session_to_code.insert(real_session_id, code.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get code by its code string
|
||||||
|
pub async fn get_code(&self, code: &str) -> Option<SupportCode> {
|
||||||
|
let codes = self.codes.read().await;
|
||||||
|
codes.get(code).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark a code as completed
|
/// Mark a code as completed
|
||||||
pub async fn mark_completed(&self, code: &str) {
|
pub async fn mark_completed(&self, code: &str) {
|
||||||
let mut codes = self.codes.write().await;
|
let mut codes = self.codes.write().await;
|
||||||
|
|||||||
@@ -269,17 +269,17 @@
|
|||||||
<div class="sidebar-panel">
|
<div class="sidebar-panel">
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="sidebar-section-title">Status</div>
|
<div class="sidebar-section-title">Status</div>
|
||||||
<div class="sidebar-item active">
|
<div class="sidebar-item active" data-filter="all">
|
||||||
<span>All Machines</span>
|
<span>All Machines</span>
|
||||||
<span class="sidebar-count">0</span>
|
<span class="sidebar-count" id="countAll">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-item">
|
<div class="sidebar-item" data-filter="online">
|
||||||
<span>Online</span>
|
<span>Online</span>
|
||||||
<span class="sidebar-count">0</span>
|
<span class="sidebar-count" id="countOnline">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-item">
|
<div class="sidebar-item" data-filter="offline">
|
||||||
<span>Offline</span>
|
<span>Offline</span>
|
||||||
<span class="sidebar-count">0</span>
|
<span class="sidebar-count" id="countOffline">0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
@@ -289,13 +289,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-panel">
|
<div class="main-panel" id="machinesList">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No machines</h3>
|
<h3>No machines</h3>
|
||||||
<p>Install the agent on a machine to see it here</p>
|
<p>Install the agent on a machine to see it here</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-panel">
|
<div class="detail-panel" id="machineDetail">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>Select a machine</h3>
|
<h3>Select a machine</h3>
|
||||||
<p>Click a machine to view details</p>
|
<p>Click a machine to view details</p>
|
||||||
@@ -476,6 +476,90 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadSessions();
|
loadSessions();
|
||||||
|
|
||||||
|
// Load connected machines (Access tab)
|
||||||
|
let machines = [];
|
||||||
|
let selectedMachine = null;
|
||||||
|
|
||||||
|
async function loadMachines() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/sessions");
|
||||||
|
machines = await response.json();
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
document.getElementById("countAll").textContent = machines.length;
|
||||||
|
document.getElementById("countOnline").textContent = machines.length;
|
||||||
|
document.getElementById("countOffline").textContent = "0";
|
||||||
|
|
||||||
|
renderMachinesList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load machines:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMachinesList() {
|
||||||
|
const container = document.getElementById("machinesList");
|
||||||
|
|
||||||
|
if (machines.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty-state"><h3>No machines</h3><p>Install the agent on a machine to see it here</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = '<div style="padding: 12px;">' + machines.map(m => {
|
||||||
|
const started = new Date(m.started_at).toLocaleString();
|
||||||
|
const isSelected = selectedMachine?.id === m.id;
|
||||||
|
return '<div class="sidebar-item' + (isSelected ? ' active' : '') + '" onclick="selectMachine(\'' + m.id + '\')" style="margin-bottom: 8px; padding: 12px;">' +
|
||||||
|
'<div style="display: flex; align-items: center; gap: 12px;">' +
|
||||||
|
'<div style="width: 10px; height: 10px; border-radius: 50%; background: hsl(142, 76%, 50%);"></div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<div style="font-weight: 500;">' + (m.agent_name || m.agent_id.slice(0,8)) + '</div>' +
|
||||||
|
'<div style="font-size: 12px; color: hsl(var(--muted-foreground));">Connected ' + started + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join("") + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMachine(id) {
|
||||||
|
selectedMachine = machines.find(m => m.id === id);
|
||||||
|
renderMachinesList();
|
||||||
|
renderMachineDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMachineDetail() {
|
||||||
|
const container = document.getElementById("machineDetail");
|
||||||
|
|
||||||
|
if (!selectedMachine) {
|
||||||
|
container.innerHTML = '<div class="empty-state"><h3>Select a machine</h3><p>Click a machine to view details</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = selectedMachine;
|
||||||
|
const started = new Date(m.started_at).toLocaleString();
|
||||||
|
|
||||||
|
container.innerHTML =
|
||||||
|
'<div class="detail-section">' +
|
||||||
|
'<div class="detail-section-title">Machine Info</div>' +
|
||||||
|
'<div class="detail-row"><span class="detail-label">Agent ID</span><span class="detail-value">' + m.agent_id.slice(0,8) + '...</span></div>' +
|
||||||
|
'<div class="detail-row"><span class="detail-label">Session ID</span><span class="detail-value">' + m.id.slice(0,8) + '...</span></div>' +
|
||||||
|
'<div class="detail-row"><span class="detail-label">Connected</span><span class="detail-value">' + started + '</span></div>' +
|
||||||
|
'<div class="detail-row"><span class="detail-label">Viewers</span><span class="detail-value">' + m.viewer_count + '</span></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="detail-section">' +
|
||||||
|
'<div class="detail-section-title">Actions</div>' +
|
||||||
|
'<button class="btn btn-primary" style="width: 100%; margin-bottom: 8px;" onclick="connectToMachine(\'' + m.id + '\')">Connect</button>' +
|
||||||
|
'<button class="btn btn-outline" style="width: 100%;" disabled>Transfer Files</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectToMachine(sessionId) {
|
||||||
|
// TODO: Open viewer in new window
|
||||||
|
alert("Viewer not yet implemented.\\n\\nSession ID: " + sessionId + "\\n\\nWebSocket: wss://connect.azcomputerguru.com/ws/viewer?session_id=" + sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh machines every 5 seconds
|
||||||
|
loadMachines();
|
||||||
|
setInterval(loadMachines, 5000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -381,16 +381,26 @@
|
|||||||
// Show instructions
|
// Show instructions
|
||||||
showInstructions();
|
showInstructions();
|
||||||
|
|
||||||
// For now, show a message that download will be available soon
|
|
||||||
// TODO: Implement actual download endpoint
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
connectBtn.querySelector('.btn-text').textContent = 'Download Starting...';
|
connectBtn.querySelector('.btn-text').textContent = 'Download Starting...';
|
||||||
|
|
||||||
// Placeholder - in production this will download the agent
|
// Create a temporary link to download the agent
|
||||||
|
// The agent will be run with the code as argument
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = '/guruconnect-agent.exe';
|
||||||
|
downloadLink.download = 'GuruConnect-' + code + '.exe';
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
|
||||||
|
// Show instructions with the code reminder
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
alert('Agent download will be available once the agent is built.\n\nSession ID: ' + sessionId);
|
connectBtn.querySelector('.btn-text').textContent = 'Run the Downloaded File';
|
||||||
connectBtn.querySelector('.btn-text').textContent = 'Connect';
|
|
||||||
}, 1000);
|
// Update instructions to include the code
|
||||||
|
instructionsList.innerHTML = getBrowserInstructions(detectBrowser()).map(step => '<li>' + step + '</li>').join('') +
|
||||||
|
'<li><strong>Important:</strong> When prompted, enter code: <strong style="color: hsl(var(--primary)); font-size: 18px;">' + code + '</strong></li>';
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
|
|||||||
Reference in New Issue
Block a user