cleanup: remove 2,369 lines of dead code

Removed backup files and unused legacy scanner function.
All code verified as unreferenced.
This commit is contained in:
Fimeg
2025-11-10 21:20:42 -05:00
parent 1f2b1b7179
commit c95cc7d91f
32 changed files with 5899 additions and 567 deletions

View File

@@ -0,0 +1,200 @@
import React, { useState } from 'react';
import { Upload, CheckCircle, XCircle, RotateCw, Download } from 'lucide-react';
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
import { Agent } from '@/types';
import { cn } from '@/lib/utils';
import toast from 'react-hot-toast';
interface AgentUpdateProps {
agent: Agent;
onUpdateComplete?: () => void;
className?: string;
}
export function AgentUpdate({ agent, onUpdateComplete, className }: AgentUpdateProps) {
const {
checkForUpdate,
triggerAgentUpdate,
updateStatus,
checkingUpdate,
updatingAgent,
hasUpdate,
availableVersion,
currentVersion
} = useAgentUpdate();
const [isChecking, setIsChecking] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
const handleCheckUpdate = async (e: React.MouseEvent) => {
e.stopPropagation();
setIsChecking(true);
try {
await checkForUpdate(agent.id);
setHasChecked(true);
if (hasUpdate && availableVersion) {
setShowConfirmDialog(true);
} else if (!hasUpdate && hasChecked) {
toast.info('Agent is already at latest version');
}
} catch (error) {
console.error('[UI] Failed to check for updates:', error);
toast.error('Failed to check for available updates');
} finally {
setIsChecking(false);
}
};
const handleConfirmUpdate = async () => {
if (!hasUpdate || !availableVersion) {
toast.error('No update available');
return;
}
setShowConfirmDialog(false);
try {
await triggerAgentUpdate(agent, availableVersion);
if (onUpdateComplete) {
onUpdateComplete();
}
} catch (error) {
console.error('[UI] Update failed:', error);
}
};
const buttonContent = () => {
if (updatingAgent) {
return (
<>
<RotateCw className="h-4 w-4 animate-spin" />
<span>
{updateStatus.status === 'downloading' && 'Downloading...'}
{updateStatus.status === 'installing' && 'Installing...'}
{updateStatus.status === 'pending' && 'Starting update...'}
</span>
</>
);
}
if (agent.is_updating) {
return (
<>
<RotateCw className="h-4 w-4 animate-pulse" />
<span>Updating...</span>
</>
);
}
if (isChecking) {
return (
<>
<RotateCw className="h-4 w-4" />
<span>Checking...</span>
</>
);
}
if (hasChecked && hasUpdate) {
return (
<>
<Download className="h-4 w-4" />
<span>Update to {availableVersion}</span>
</>
);
}
return (
<>
<Upload className="h-4 w-4" />
<span>Check for Update</span>
</>
);
};
return (
<div className="inline-flex items-center">
<button
onClick={handleCheckUpdate}
disabled={updatingAgent || agent.is_updating || isChecking}
className={cn(
"text-sm px-3 py-1 rounded border flex items-center space-x-2 transition-colors",
{
"bg-green-50 border-green-300 text-green-700 hover:bg-green-100": hasChecked && hasUpdate,
"bg-amber-50 border-amber-300 text-amber-700": updatingAgent || agent.is_updating,
"text-gray-600 hover:text-primary-600 border-gray-300 bg-white hover:bg-primary-50":
!updatingAgent && !agent.is_updating && !hasUpdate
},
className
)}
title={updatingAgent ? "Updating agent..." : agent.is_updating ? "Agent is updating..." : "Check for available updates"}
>
{buttonContent()}
</button>
{/* Progress indicator */}
{updatingAgent && updateStatus.progress && (
<div className="ml-2 w-16 h-2 bg-gray-200 rounded">
<div
className="h-2 bg-green-500 rounded"
style={{ width: `${updateStatus.progress}%` }}
/>
</div>
)}
{/* Status icon */}
{hasChecked && !updatingAgent && (
<div className="ml-2">
{hasUpdate ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-gray-400" />
)}
</div>
)}
{/* Version info popup */}
{hasChecked && (
<div className="ml-2 text-xs text-gray-500">
{currentVersion} {hasUpdate ? availableVersion : 'Latest'}
</div>
)}
{/* Confirmation Dialog */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg max-w-md mx-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900">
Update Agent: {agent.hostname}
</h3>
<p className="mb-4 text-gray-600">
Update agent from <strong>{currentVersion}</strong> to <strong>{availableVersion}</strong>?
</p>
<p className="mb-4 text-sm text-gray-500">
This will temporarily take the agent offline during the update process.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowConfirmDialog(false)}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleConfirmUpdate}
className="px-4 py-2 bg-primary-600 text-white rounded hover:bg-primary-700"
>
Update Agent
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,208 @@
import React, { useState } from 'react';
import { Upload, RefreshCw } from 'lucide-react';
import { agentApi } from '@/lib/api';
import { Agent } from '@/types';
import toast from 'react-hot-toast';
interface BulkAgentUpdateProps {
agents: Agent[];
onBulkUpdateComplete?: () => void;
}
export function BulkAgentUpdate({ agents, onBulkUpdateComplete }: BulkAgentUpdateProps) {
const [updatingAgents, setUpdatingAgents] = useState<Set<string>>(new Set());
const [checkingUpdates, setCheckingUpdates] = useState<Set<string>>(new Set());
const handleBulkUpdate = async () => {
if (agents.length === 0) {
toast.error('No agents selected');
return;
}
// Check each agent for available updates first
let agentsNeedingUpdate: Agent[] = [];
let availableVersion: string | undefined;
// This will populate the checking state
agents.forEach(agent => setCheckingUpdates(prev => new Set(prev).add(agent.id)));
try {
const checkPromises = agents.map(async (agent) => {
try {
const result = await agentApi.checkForUpdateAvailable(agent.id);
if (result.hasUpdate && result.latestVersion) {
agentsNeedingUpdate.push(agent);
if (!availableVersion) {
availableVersion = result.latestVersion;
}
}
} catch (error) {
console.error(`Failed to check updates for agent ${agent.id}:`, error);
} finally {
setCheckingUpdates(prev => {
const newSet = new Set(prev);
newSet.delete(agent.id);
return newSet;
});
}
});
await Promise.all(checkPromises);
if (agentsNeedingUpdate.length === 0) {
toast.info('Selected agents are already up to date');
return;
}
// Generate nonces for each agent that needs updating
const noncePromises = agentsNeedingUpdate.map(async (agent) => {
if (availableVersion) {
try {
const nonceData = await agentApi.generateUpdateNonce(agent.id, availableVersion);
// Store nonce for use in update request
return {
agentId: agent.id,
hostname: agent.hostname,
nonce: nonceData.update_nonce,
targetVersion: availableVersion
};
} catch (error) {
console.error(`Failed to generate nonce for ${agent.hostname}:`, error);
return null;
}
}
return null;
});
const nonceResults = await Promise.all(noncePromises);
const validUpdates = nonceResults.filter(item => item !== null);
if (validUpdates.length === 0) {
toast.error('Failed to generate update nonces for any agents');
return;
}
// Perform bulk updates
const updateData = {
agent_ids: validUpdates.map(item => item.agentId),
version: availableVersion,
platform: 'linux-amd64', // This should match the platform
nonces: validUpdates.map(item => item.nonce)
};
// Mark agents as updating
validUpdates.forEach(item => {
setUpdatingAgents(prev => new Set(prev).add(item.agentId));
});
const result = await agentApi.updateMultipleAgents(updateData);
toast.success(`Initiated updates for ${result.updated.length} of ${agents.length} agents`);
if (result.failed.length > 0) {
toast.error(`Failed to update ${result.failed.length} agents`);
}
// Start polling for completion
startBulkUpdatePolling(validUpdates);
if (onBulkUpdateComplete) {
onBulkUpdateComplete();
}
} catch (error) {
console.error('Bulk update failed:', error);
toast.error(`Bulk update failed: ${error.message}`);
}
};
const startBulkUpdatePolling = (agents: Array<{agentId: string, hostname: string}>) => {
let attempts = 0;
const maxAttempts = 60; // 5 minutes max
const pollInterval = setInterval(async () => {
attempts++;
if (attempts >= maxAttempts || updatingAgents.size === 0) {
clearInterval(pollInterval);
setUpdatingAgents(new Set());
return;
}
const statusPromises = agents.map(async (item) => {
try {
const status = await agentApi.getUpdateStatus(item.agentId);
if (status.status === 'complete' || status.status === 'failed') {
// Remove from updating set
setUpdatingAgents(prev => {
const newSet = new Set(prev);
newSet.delete(item.agentId);
return newSet;
});
if (status.status === 'complete') {
toast.success(`${item.hostname} updated successfully`);
} else {
toast.error(`${item.hostname} update failed: ${status.error || 'Unknown error'}`);
}
}
} catch (error) {
console.error(`Failed to poll ${item.hostname}:`, error);
}
});
await Promise.allSettled(statusPromises);
}, 5000); // Check every 5 seconds
return () => clearInterval(pollInterval);
};
const isAnyAgentUpdating = (): boolean => {
return agents.some(agent => updatingAgents.has(agent.id));
};
const isAnyAgentChecking = (): boolean => {
return agents.some(agent => checkingUpdates.has(agent.id));
};
const getButtonContent = () => {
if (isAnyAgentUpdating() || isAnyAgentChecking()) {
return (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
<span>{isAnyAgentChecking() ? "Checking..." : "Updating..."}</span>
</>
);
}
if (agents.length === 1) {
return (
<>
<Upload className="h-4 w-4" />
<span>Update 1 Agent</span>
</>
);
}
return (
<>
<Upload className="h-4 w-4" />
<span>Update {agents.length} Agents</span>
</>
);
};
return (
<button
onClick={handleBulkUpdate}
disabled={isAnyAgentUpdating() || isAnyAgentChecking()}
className="btn btn-secondary"
>
{getButtonContent()}
</button>
);
}

View File

@@ -0,0 +1,159 @@
import { useState, useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';
import { agentApi } from '@/lib/api';
import { Agent } from '@/types';
interface UseAgentUpdateReturn {
checkForUpdate: (agentId: string) => Promise<void>;
triggerAgentUpdate: (agent: Agent, targetVersion: string) => Promise<void>;
updateStatus: UpdateStatus;
checkingUpdate: boolean;
updatingAgent: boolean;
hasUpdate: boolean;
availableVersion?: string;
currentVersion?: string;
}
interface UpdateStatus {
status: 'idle' | 'checking' | 'pending' | 'downloading' | 'installing' | 'complete' | 'failed';
progress?: number;
error?: string;
}
export function useAgentUpdate(): UseAgentUpdateReturn {
const queryClient = useQueryClient();
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>({ status: 'idle' });
const [hasUpdate, setHasUpdate] = useState(false);
const [availableVersion, setAvailableVersion] = useState<string>();
const [currentVersion, setCurrentVersion] = useState<string>();
// Check if update available for agent
const checkMutation = useMutation({
mutationFn: agentApi.checkForUpdateAvailable,
onSuccess: (data) => {
setHasUpdate(data.hasUpdate);
setAvailableVersion(data.latestVersion);
setCurrentVersion(data.currentVersion);
if (!data.hasUpdate) {
toast.info('Agent is already at latest version');
}
},
onError: (error) => {
console.error('Failed to check for updates:', error);
toast.error(`Failed to check for updates: ${error.message}`);
}
});
// Check for update available
const checkForUpdate = async (agentId: string) => {
try {
await checkMutation.mutateAsync(agentId);
} catch (error) {
console.error('Error checking for update:', error);
}
};
// Trigger agent update with nonce generation
const triggerAgentUpdate = async (agent: Agent, targetVersion: string) => {
try {
// Step 1: Check for update availability (already done by checkmutation)
if (!hasUpdate) {
await checkForUpdate(agent.id);
if (!hasUpdate) {
toast.info('No updates available');
return;
}
}
// Step 2: Generate nonce for authorized update
const nonceData = await agentApi.generateUpdateNonce(agent.id, targetVersion);
console.log('[UI] Update nonce generated:', nonceData);
// Step 3: Trigger the actual update
const updateResponse = await agentApi.updateAgent(agent.id, {
version: targetVersion,
platform: `${agent.os_type}-${agent.os_architecture}`,
// Include nonce in request for security
nonce: nonceData.update_nonce
});
setUpdateStatus({ status: 'pending', progress: 0 });
// Step 4: Start polling for progress
startUpdatePolling(agent.id);
// Step 5: Refresh agent data in cache
queryClient.invalidateQueries({ queryKey: ['agents'] });
console.log('[UI] Update initiated successfully:', updateResponse);
} catch (error) {
console.error('[UI] Update failed:', error);
toast.error(`Update failed: ${error.message}`);
setUpdateStatus({ status: 'failed', error: error.message });
}
};
// Poll for update progress
const startUpdatePolling = (agentId: string) => {
let attempts = 0;
const maxAttempts = 60; // 5 minutes with 5 second intervals
const pollInterval = setInterval(async () => {
attempts++;
if (attempts >= maxAttempts) {
clearInterval(pollInterval);
setUpdateStatus({ status: 'failed', error: 'Update timeout' });
toast.error('Update timed out after 5 minutes');
return;
}
try {
const status = await agentApi.getUpdateStatus(agentId);
switch (status.status) {
case 'complete':
clearInterval(pollInterval);
setUpdateStatus({ status: 'complete' });
toast.success('Agent updated successfully!');
setHasUpdate(false);
setAvailableVersion(undefined);
break;
case 'failed':
clearInterval(pollInterval);
setUpdateStatus({ status: 'failed', error: status.error || 'Update failed' });
toast.error(`Update failed: ${status.error || 'Unknown error'}`);
break;
case 'downloading':
setUpdateStatus({ status: 'downloading', progress: status.progress });
break;
case 'installing':
setUpdateStatus({ status: 'installing', progress: status.progress });
break;
default:
setUpdateStatus({ status: 'idle' });
}
} catch (error) {
console.error('[UI] Failed to get update status:', error);
clearInterval(pollInterval);
setUpdateStatus({ status: 'failed', error: 'Failed to get update status' });
}
}, 5000); // Poll every 5 seconds
return () => clearInterval(pollInterval);
};
return {
checkForUpdate,
triggerAgentUpdate,
updateStatus,
updatingAgent: updateStatus.status === 'downloading' || updateStatus.status === 'installing' || updateStatus.status === 'pending',
hasUpdate,
availableVersion,
currentVersion
};
}

View File

@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
export interface ServerKeySecurityStatus {
has_private_key: boolean;
public_key_fingerprint?: string;
algorithm?: string;
}
export const useServerKeySecurity = () => {
return useQuery<ServerKeySecurityStatus, Error>({
queryKey: ['serverKeySecurity'],
queryFn: async () => {
const response = await api.get('/security/overview');
const overview = response.data;
const signingStatus = overview.subsystems.ed25519_signing;
return {
has_private_key: signingStatus.status === 'healthy',
public_key_fingerprint: signingStatus.checks?.public_key_fingerprint,
algorithm: signingStatus.checks?.algorithm,
};
},
});
};

View File

@@ -28,11 +28,13 @@ import {
Upload,
} from 'lucide-react';
import { useAgents, useAgent, useScanAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
import { useActiveCommands, useCancelCommand } from '@/hooks/useCommands';
import { useHeartbeatStatus, useInvalidateHeartbeat, useHeartbeatAgentSync } from '@/hooks/useHeartbeat';
import { agentApi } from '@/lib/api';
import { useQueryClient } from '@tanstack/react-query';
import { getStatusColor, formatRelativeTime, isOnline, formatBytes } from '@/lib/utils';
import { AgentUpdate } from '@/components/AgentUpdate';
import { cn } from '@/lib/utils';
import toast from 'react-hot-toast';
import { AgentSystemUpdates } from '@/components/AgentUpdates';
@@ -40,6 +42,7 @@ import { AgentStorage } from '@/components/AgentStorage';
import { AgentUpdatesEnhanced } from '@/components/AgentUpdatesEnhanced';
import { AgentScanners } from '@/components/AgentScanners';
import { AgentUpdatesModal } from '@/components/AgentUpdatesModal';
import { BulkAgentUpdate } from '@/components/RelayList';
import ChatTimeline from '@/components/ChatTimeline';
const Agents: React.FC = () => {
@@ -1167,13 +1170,12 @@ const Agents: React.FC = () => {
)}
Scan Selected ({selectedAgents.length})
</button>
<button
onClick={() => setShowUpdateModal(true)}
className="btn btn-secondary"
>
<Upload className="h-4 w-4 mr-2" />
Update Selected ({selectedAgents.length})
</button>
<BulkAgentUpdate
agents={agents.filter(agent => selectedAgents.includes(agent.id))}
onBulkUpdateComplete={() => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
}}
/>
</>
)}
</div>
@@ -1393,6 +1395,13 @@ const Agents: React.FC = () => {
>
<Upload className="h-4 w-4" />
</button>
{/* Agent Update with nonce security */}
<AgentUpdate
agent={agent}
onUpdateComplete={() => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
}}
/>
<button
onClick={() => handleRemoveAgent(agent.id, agent.hostname)}
disabled={unregisterAgentMutation.isPending}

View File

@@ -11,8 +11,11 @@ import {
} from 'lucide-react';
import { useDashboardStats } from '@/hooks/useStats';
import { useServerKeySecurity } from '@/hooks/useSecurity';
const Dashboard: React.FC = () => {
const { data: stats, isPending, error } = useDashboardStats();
const { data: serverKeySecurity } = useServerKeySecurity();
if (isPending) {
return (
@@ -95,6 +98,19 @@ const Dashboard: React.FC = () => {
</p>
</div>
{/* Important Messages / Security Alert */}
{serverKeySecurity && !serverKeySecurity.has_private_key && (
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-lg relative mb-8" role="alert">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 mr-3" />
<div>
<strong className="font-bold">Security Upgrade Required:</strong>
<span className="block sm:inline"> Your server is missing a private key for secure agent updates. Please go to <Link to="/settings/agents" className="font-bold underline hover:text-yellow-900">Agent Management</Link> to generate one.</span>
</div>
</div>
</div>
)}
{/* Stats cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{statCards.map((stat) => {

View File

@@ -18,11 +18,15 @@ import {
import { useRegistrationTokens } from '@/hooks/useRegistrationTokens';
import { toast } from 'react-hot-toast';
import { useServerKeySecurity } from '@/hooks/useSecurity';
const AgentManagement: React.FC = () => {
const navigate = useNavigate();
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
const [selectedPlatform, setSelectedPlatform] = useState<string>('linux');
const { data: tokens, isLoading: tokensLoading } = useRegistrationTokens({ is_active: true });
const { data: serverKeySecurity, isLoading: isLoadingServerKeySecurity, refetch: refetchServerKeySecurity } = useServerKeySecurity();
const [generatingKeys, setGeneratingKeys] = useState(false);
const platforms = [
{
@@ -303,6 +307,88 @@ const AgentManagement: React.FC = () => {
</div>
<div className="space-y-6">
{/* Server Signing Key */}
<div>
<h4 className="font-medium text-gray-900 mb-3">🔑 Server Signing Key</h4>
{isLoadingServerKeySecurity ? (
<div className="text-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-sm text-gray-500 mt-2">Loading key status...</p>
</div>
) : serverKeySecurity?.has_private_key ? (
<div className="space-y-3">
<div className="bg-green-50 border border-green-200 rounded-md p-3">
<p className="text-sm text-green-800">
Server has a private key for signing agent updates.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Public Key Fingerprint
</label>
<input
readOnly
value={serverKeySecurity.public_key_fingerprint}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Algorithm
</label>
<input
readOnly
value={serverKeySecurity.algorithm?.toUpperCase()}
className="block w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md text-sm"
/>
</div>
</div>
) : (
<div className="space-y-3">
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
<p className="text-sm text-yellow-800">
Your server is missing a private key. Generate one to enable secure agent updates.
</p>
</div>
<button
type="button"
onClick={async () => {
setGeneratingKeys(true);
try {
const response = await fetch('/api/setup/generate-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error('Failed to generate keys');
}
toast.success('Signing keys generated successfully! Please restart your server.');
refetchServerKeySecurity(); // Refresh status
} catch (error: any) {
toast.error(error.message || 'Failed to generate keys');
} finally {
setGeneratingKeys(false);
}
}}
disabled={generatingKeys}
className="w-full py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{generatingKeys ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Generating Keys...
</>
) : (
<>
<Key className="h-4 w-4 mr-2" />
Generate Signing Keys
</>
)}
</button>
</div>
)}
</div>
<div>
<h4 className="font-medium text-gray-900 mb-3">🛡 Security Model</h4>
<p className="text-sm text-gray-600 mb-4">