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:
@@ -24,6 +24,10 @@ pub struct AgentParams {
|
||||
agent_id: String,
|
||||
#[serde(default)]
|
||||
agent_name: Option<String>,
|
||||
#[serde(default)]
|
||||
support_code: Option<String>,
|
||||
#[serde(default)]
|
||||
hostname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -38,10 +42,12 @@ pub async fn agent_ws_handler(
|
||||
Query(params): Query<AgentParams>,
|
||||
) -> impl IntoResponse {
|
||||
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 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
|
||||
@@ -60,8 +66,10 @@ pub async fn viewer_ws_handler(
|
||||
async fn handle_agent_connection(
|
||||
socket: WebSocket,
|
||||
sessions: SessionManager,
|
||||
support_codes: crate::support_codes::SupportCodeManager,
|
||||
agent_id: String,
|
||||
agent_name: String,
|
||||
support_code: Option<String>,
|
||||
) {
|
||||
info!("Agent connected: {} ({})", agent_name, agent_id);
|
||||
|
||||
@@ -70,6 +78,13 @@ async fn handle_agent_connection(
|
||||
|
||||
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();
|
||||
|
||||
// Task to forward input events from viewers to agent
|
||||
@@ -82,6 +97,8 @@ async fn handle_agent_connection(
|
||||
});
|
||||
|
||||
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
|
||||
while let Some(msg) = ws_receiver.next().await {
|
||||
@@ -119,6 +136,13 @@ async fn handle_agent_connection(
|
||||
// Cleanup
|
||||
input_forward.abort();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
pub async fn mark_completed(&self, code: &str) {
|
||||
let mut codes = self.codes.write().await;
|
||||
|
||||
@@ -269,17 +269,17 @@
|
||||
<div class="sidebar-panel">
|
||||
<div class="sidebar-section">
|
||||
<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 class="sidebar-count">0</span>
|
||||
<span class="sidebar-count" id="countAll">0</span>
|
||||
</div>
|
||||
<div class="sidebar-item">
|
||||
<div class="sidebar-item" data-filter="online">
|
||||
<span>Online</span>
|
||||
<span class="sidebar-count">0</span>
|
||||
<span class="sidebar-count" id="countOnline">0</span>
|
||||
</div>
|
||||
<div class="sidebar-item">
|
||||
<div class="sidebar-item" data-filter="offline">
|
||||
<span>Offline</span>
|
||||
<span class="sidebar-count">0</span>
|
||||
<span class="sidebar-count" id="countOffline">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
@@ -289,13 +289,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-panel">
|
||||
<div class="main-panel" id="machinesList">
|
||||
<div class="empty-state">
|
||||
<h3>No machines</h3>
|
||||
<p>Install the agent on a machine to see it here</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-panel">
|
||||
<div class="detail-panel" id="machineDetail">
|
||||
<div class="empty-state">
|
||||
<h3>Select a machine</h3>
|
||||
<p>Click a machine to view details</p>
|
||||
@@ -476,6 +476,90 @@
|
||||
}
|
||||
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -381,16 +381,26 @@
|
||||
// Show instructions
|
||||
showInstructions();
|
||||
|
||||
// For now, show a message that download will be available soon
|
||||
// TODO: Implement actual download endpoint
|
||||
setLoading(false);
|
||||
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(() => {
|
||||
alert('Agent download will be available once the agent is built.\n\nSession ID: ' + sessionId);
|
||||
connectBtn.querySelector('.btn-text').textContent = 'Connect';
|
||||
}, 1000);
|
||||
connectBtn.querySelector('.btn-text').textContent = 'Run the Downloaded File';
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user