Enhance deployment system with retry functionality and improved UX

Major Improvements:
- Added retry deployment buttons in machine list for failed deployments
- Added retry button in SSH console modal footer for enhanced UX
- Enhanced deployment process with comprehensive cleanup of existing services
- Improved binary installation with password-based sudo authentication
- Updated configuration generation to include all required sections (agent, ai, network, security)
- Fixed deployment verification and error handling

Security Enhancements:
- Enhanced verifiedStopExistingServices with thorough cleanup process
- Improved binary copying with proper sudo authentication
- Added comprehensive configuration validation

UX Improvements:
- Users can retry deployments without re-running machine discovery
- Retry buttons available from both machine list and console modal
- Real-time deployment progress with detailed console output
- Clear error states with actionable retry options

Technical Changes:
- Modified ServiceDeployment.tsx with retry button components
- Enhanced api/setup_manager.go with improved deployment functions
- Updated main.go with command line argument support (--config, --setup)
- Added comprehensive zero-trust security validation system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-08-31 10:23:27 +10:00
parent df4d98bf30
commit be761cfe20
234 changed files with 7508 additions and 38528 deletions

View File

@@ -149,10 +149,29 @@ export default function AIConfiguration({
setValidatingLocal(true)
try {
const response = await fetch(`${config.localAIEndpoint}/api/tags`)
setLocalAIValid(response.ok)
const response = await fetch('/api/setup/ollama/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: config.localAIEndpoint
})
})
const result = await response.json()
if (result.valid && result.models) {
setLocalAIValid(true)
// Update the local AI models list with discovered models
setConfig(prev => ({ ...prev, localAIModels: result.models }))
} else {
setLocalAIValid(false)
console.error('Ollama validation failed:', result.message)
}
} catch (error) {
setLocalAIValid(false)
console.error('Ollama validation error:', error)
} finally {
setValidatingLocal(false)
}
@@ -232,26 +251,26 @@ export default function AIConfiguration({
</h3>
<div className={`p-4 rounded-lg border mb-4 ${
gpuRecommendation.type === 'success' ? 'bg-green-50 border-green-200' :
gpuRecommendation.type === 'success' ? 'bg-eucalyptus-50 border-eucalyptus-950' :
gpuRecommendation.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
'bg-blue-50 border-blue-200'
}`}>
<div className="flex items-start">
<InformationCircleIcon className={`h-5 w-5 mt-0.5 mr-2 ${
gpuRecommendation.type === 'success' ? 'text-green-600' :
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-600' :
'text-blue-600'
}`} />
<div>
<div className={`font-medium ${
gpuRecommendation.type === 'success' ? 'text-green-800' :
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-800' :
'text-blue-800'
}`}>
{gpuRecommendation.recommendation}
</div>
<div className={`text-sm mt-1 ${
gpuRecommendation.type === 'success' ? 'text-green-700' :
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-700' :
'text-blue-700'
}`}>
@@ -376,7 +395,7 @@ export default function AIConfiguration({
</button>
</div>
{localAIValid === true && (
<div className="flex items-center mt-1 text-green-600 text-sm">
<div className="flex items-center mt-1 text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Connection successful
</div>
@@ -468,7 +487,7 @@ export default function AIConfiguration({
</button>
</div>
{openaiValid === true && (
<div className="flex items-center mt-1 text-green-600 text-sm">
<div className="flex items-center mt-1 text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
API key valid
</div>

View File

@@ -141,7 +141,7 @@ export default function LicenseValidation({
<div className="flex items-center mb-4">
<KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">License Information</h3>
{validationResult?.valid && <CheckCircleIcon className="h-5 w-5 text-green-500 ml-2" />}
{validationResult?.valid && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
</div>
<div className="space-y-4">
@@ -230,7 +230,7 @@ export default function LicenseValidation({
<div className="flex items-start">
<div className="flex-shrink-0">
{validationResult.valid ? (
<CheckCircleIcon className="h-6 w-6 text-eucalyptus-950 dark:text-eucalyptus-50" />
<CheckCircleIcon className="h-6 w-6 text-eucalyptus-600 dark:text-eucalyptus-50" />
) : (
<ExclamationTriangleIcon className="h-6 w-6 text-coral-950 dark:text-coral-50" />
)}

View File

@@ -347,16 +347,16 @@ export default function RepositoryConfiguration({
{validation && (
<div className={`flex items-center p-3 rounded-lg mb-4 ${
validation.valid
? 'bg-green-50 border border-green-200'
? 'bg-eucalyptus-50 border border-eucalyptus-950'
: 'bg-red-50 border border-red-200'
}`}>
{validation.valid ? (
<CheckCircleIcon className="h-5 w-5 text-green-600 mr-2" />
<CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 mr-2" />
) : (
<XCircleIcon className="h-5 w-5 text-red-600 mr-2" />
)}
<span className={`text-sm ${
validation.valid ? 'text-green-800' : 'text-red-800'
validation.valid ? 'text-eucalyptus-600' : 'text-red-800'
}`}>
{validation.valid ? validation.message : validation.error}
</span>

View File

@@ -208,7 +208,7 @@ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFwwAAAAd...
<div className="flex items-center mb-4">
<KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">SSH Key Management</h3>
{validation.sshKeys === true && <CheckCircleIcon className="h-5 w-5 text-green-500 ml-2" />}
{validation.sshKeys === true && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
{validation.sshKeys === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
</div>
@@ -420,7 +420,7 @@ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFwwAAAAd...
<div className="flex items-center mb-4">
<LockClosedIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">TLS/SSL Configuration</h3>
{validation.tlsCert === true && <CheckCircleIcon className="h-5 w-5 text-green-500 ml-2" />}
{validation.tlsCert === true && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
{validation.tlsCert === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
</div>
@@ -626,7 +626,7 @@ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFwwAAAAd...
className="w-full p-3 border border-gray-300 rounded-lg"
/>
{configData?.network && (
<p className="text-sm text-green-600 mt-1 flex items-center">
<p className="text-sm text-eucalyptus-600 mt-1 flex items-center">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Ports automatically configured from Network Settings: {[
configData.network.bzzzPort,

View File

@@ -380,10 +380,10 @@ export default function ServiceDeployment({
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <CheckCircleIcon className="h-5 w-5 text-green-500" />
case 'connected': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
case 'failed': return <XCircleIcon className="h-5 w-5 text-red-500" />
case 'testing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
case 'running': return <CheckCircleIcon className="h-5 w-5 text-green-500" />
case 'running': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
case 'installing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
case 'error': return <XCircleIcon className="h-5 w-5 text-red-500" />
case 'stopped': return <StopIcon className="h-5 w-5 text-yellow-500" />
@@ -585,12 +585,24 @@ export default function ServiceDeployment({
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-green-600 hover:text-green-900"
className="text-eucalyptus-600 hover:text-eucalyptus-600"
>
Install
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'error' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-amber-600 hover:text-amber-700 mr-2"
title="Retry deployment"
>
<ArrowPathIcon className="h-4 w-4 inline mr-1" />
Retry
</button>
)}
{machine.deployStatus !== 'not_deployed' && (
<>
<button
@@ -684,7 +696,7 @@ export default function ServiceDeployment({
</button>
</div>
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm max-h-64 overflow-y-auto">
<div className="bg-gray-900 text-eucalyptus-600 p-4 rounded font-mono text-sm max-h-64 overflow-y-auto">
{deploymentLogs[showLogs]?.map((log, index) => (
<div key={index}>{log}</div>
)) || <div>No logs available</div>}
@@ -699,7 +711,7 @@ export default function ServiceDeployment({
<div className="bg-gray-900 rounded-lg overflow-hidden max-w-4xl w-full max-h-[80vh] flex flex-col">
<div className="bg-gray-800 px-4 py-3 flex justify-between items-center border-b border-gray-700">
<div className="flex items-center">
<ComputerDesktopIcon className="h-5 w-5 text-green-400 mr-2" />
<ComputerDesktopIcon className="h-5 w-5 text-eucalyptus-600 mr-2" />
<h3 className="text-lg font-medium text-white">
SSH Console - {machines.find(m => m.id === showConsole)?.hostname}
</h3>
@@ -711,7 +723,7 @@ export default function ServiceDeployment({
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<div className="w-2 h-2 bg-eucalyptus-500 rounded-full"></div>
</div>
<button
onClick={() => setShowConsole(null)}
@@ -722,7 +734,7 @@ export default function ServiceDeployment({
</div>
</div>
<div className="flex-1 p-4 font-mono text-sm overflow-y-auto bg-gray-900">
<div className="text-green-400 space-y-1">
<div className="text-eucalyptus-600 space-y-1">
{consoleLogs[showConsole]?.length > 0 ? (
consoleLogs[showConsole].map((log, index) => (
<div key={index} className="whitespace-pre-wrap">{log}</div>
@@ -734,10 +746,26 @@ export default function ServiceDeployment({
<div className="inline-block w-2 h-4 bg-green-400 animate-pulse"></div>
</div>
</div>
<div className="bg-gray-800 px-4 py-2 border-t border-gray-700">
<div className="bg-gray-800 px-4 py-2 border-t border-gray-700 flex justify-between items-center">
<div className="text-xs text-gray-400">
💡 This console shows real-time deployment progress and SSH operations
</div>
{(() => {
const machine = machines.find(m => m.id === showConsole)
return machine?.sshStatus === 'connected' && machine?.deployStatus === 'error' && (
<button
type="button"
onClick={() => {
deployToMachine(showConsole!)
}}
className="ml-4 px-3 py-1 bg-amber-600 hover:bg-amber-700 text-white text-xs rounded-md flex items-center space-x-1 transition-colors"
title="Retry deployment"
>
<ArrowPathIcon className="h-3 w-3" />
<span>Retry Deployment</span>
</button>
)
})()}
</div>
</div>
</div>

View File

@@ -94,7 +94,7 @@ export default function SystemDetection({
const getStatusColor = (condition: boolean) => {
return condition ? 'text-green-600' : 'text-red-600'
return condition ? 'text-eucalyptus-600' : 'text-red-600'
}
const getStatusIcon = (condition: boolean) => {
@@ -106,7 +106,7 @@ export default function SystemDetection({
<div className="flex items-center justify-center py-12">
<div className="text-center">
<ArrowPathIcon className="h-8 w-8 text-bzzz-primary animate-spin mx-auto mb-4" />
<p className="text-gray-600">Detecting system configuration...</p>
<p className="text-chorus-text-secondary">Detecting system configuration...</p>
</div>
</div>
)
@@ -116,10 +116,10 @@ export default function SystemDetection({
return (
<div className="text-center py-12">
<ExclamationTriangleIcon className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
<h3 className="heading-subsection mb-2">
System Detection Failed
</h3>
<p className="text-gray-600 mb-4">
<p className="text-chorus-text-secondary mb-4">
Unable to detect system configuration. Please try again.
</p>
<button
@@ -136,9 +136,9 @@ export default function SystemDetection({
return (
<div className="space-y-6">
{/* System Overview */}
<div className="bg-gray-50 rounded-lg p-6">
<div className="card">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">System Overview</h3>
<h3 className="heading-subsection">System Overview</h3>
<button
onClick={refreshSystemInfo}
disabled={refreshing}
@@ -150,12 +150,12 @@ export default function SystemDetection({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-gray-700">Hostname</div>
<div className="text-lg text-gray-900">{detectedInfo.network.hostname}</div>
<div className="text-sm font-medium text-chorus-text-secondary">Hostname</div>
<div className="text-lg text-chorus-text-primary">{detectedInfo.network.hostname}</div>
</div>
<div>
<div className="text-sm font-medium text-gray-700">Operating System</div>
<div className="text-lg text-gray-900">
<div className="text-sm font-medium text-chorus-text-secondary">Operating System</div>
<div className="text-lg text-chorus-text-primary">
{detectedInfo.os} ({detectedInfo.architecture})
</div>
</div>
@@ -165,22 +165,22 @@ export default function SystemDetection({
{/* Hardware Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* CPU & Memory */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="card">
<div className="flex items-center mb-4">
<CpuChipIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">CPU & Memory</h3>
<h3 className="heading-subsection">CPU & Memory</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-gray-700">CPU</div>
<div className="text-gray-900">
<div className="text-sm font-medium text-chorus-text-secondary">CPU</div>
<div className="text-chorus-text-primary">
{detectedInfo.cpu_cores} cores
</div>
</div>
<div>
<div className="text-sm font-medium text-gray-700">Memory</div>
<div className="text-gray-900">
<div className="text-sm font-medium text-chorus-text-secondary">Memory</div>
<div className="text-chorus-text-primary">
{Math.round(detectedInfo.memory_mb / 1024)} GB total
</div>
</div>
@@ -188,21 +188,21 @@ export default function SystemDetection({
</div>
{/* Storage */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="card">
<div className="flex items-center mb-4">
<CircleStackIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">Storage</h3>
<h3 className="heading-subsection">Storage</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-gray-700">Disk Space</div>
<div className="text-gray-900">
<div className="text-sm font-medium text-chorus-text-secondary">Disk Space</div>
<div className="text-chorus-text-primary">
{detectedInfo.storage.total_space_gb} GB total, {' '}
{detectedInfo.storage.free_space_gb} GB available
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="w-full bg-chorus-border-invisible rounded-full h-2">
<div
className="bg-bzzz-primary h-2 rounded-full"
style={{
@@ -216,19 +216,19 @@ export default function SystemDetection({
{/* GPU Information */}
{detectedInfo.gpus && detectedInfo.gpus.length > 0 && (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="card">
<div className="flex items-center mb-4">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">
<h3 className="heading-subsection">
GPU Configuration ({detectedInfo.gpus.length} GPU{detectedInfo.gpus.length !== 1 ? 's' : ''})
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{detectedInfo.gpus.map((gpu, index) => (
<div key={index} className="bg-gray-50 rounded-lg p-4">
<div className="font-medium text-gray-900">{gpu.name}</div>
<div className="text-sm text-gray-600">
<div key={index} className="bg-chorus-warm rounded-lg p-4">
<div className="font-medium text-chorus-text-primary">{gpu.name}</div>
<div className="text-sm text-chorus-text-secondary">
{gpu.type.toUpperCase()} {gpu.memory} {gpu.driver}
</div>
</div>
@@ -238,21 +238,21 @@ export default function SystemDetection({
)}
{/* Network Information */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="card">
<div className="flex items-center mb-4">
<GlobeAltIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">Network Configuration</h3>
<h3 className="heading-subsection">Network Configuration</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-gray-700">Hostname</div>
<div className="text-gray-900">{detectedInfo.network.hostname}</div>
<div className="text-sm font-medium text-chorus-text-secondary">Hostname</div>
<div className="text-chorus-text-primary">{detectedInfo.network.hostname}</div>
</div>
{detectedInfo.network.private_ips && detectedInfo.network.private_ips.length > 0 && (
<div>
<div className="text-sm font-medium text-gray-700 mb-2">Private IP Addresses</div>
<div className="text-sm font-medium text-chorus-text-secondary mb-2">Private IP Addresses</div>
<div className="space-y-2">
{detectedInfo.network.private_ips.map((ip, index) => (
<div key={index} className="flex justify-between items-center text-sm">
@@ -266,16 +266,16 @@ export default function SystemDetection({
{detectedInfo.network.public_ip && (
<div>
<div className="text-sm font-medium text-gray-700">Public IP</div>
<div className="text-gray-900">{detectedInfo.network.public_ip}</div>
<div className="text-sm font-medium text-chorus-text-secondary">Public IP</div>
<div className="text-chorus-text-primary">{detectedInfo.network.public_ip}</div>
</div>
)}
</div>
</div>
{/* Software Requirements */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Software Requirements</h3>
<div className="card">
<h3 className="heading-subsection mb-4">Software Requirements</h3>
<div className="space-y-4">
{[
@@ -304,9 +304,9 @@ export default function SystemDetection({
<div className="flex items-center">
<StatusIcon className={`h-5 w-5 mr-3 ${getStatusColor(software.installed)}`} />
<div>
<div className="font-medium text-gray-900">{software.name}</div>
<div className="font-medium text-chorus-text-primary">{software.name}</div>
{software.version && (
<div className="text-sm text-gray-600">Version: {software.version}</div>
<div className="text-sm text-chorus-text-secondary">Version: {software.version}</div>
)}
</div>
</div>
@@ -327,8 +327,8 @@ export default function SystemDetection({
</div>
{/* System Validation */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-blue-900 mb-4">System Validation</h3>
<div className="panel panel-info">
<h3 className="heading-subsection mb-4 panel-title">System Validation</h3>
<div className="space-y-2">
{[
@@ -351,13 +351,13 @@ export default function SystemDetection({
<div key={index} className="flex items-center">
<StatusIcon className={`h-4 w-4 mr-3 ${
validation.passed
? 'text-green-600'
? 'text-eucalyptus-600'
: 'text-red-600'
}`} />
<span className={`text-sm ${
validation.passed
? 'text-green-800'
: 'text-red-800'
? 'text-eucalyptus-600'
: 'text-red-600'
}`}>
{validation.check}
{validation.warning && validation.passed && (
@@ -371,7 +371,7 @@ export default function SystemDetection({
</div>
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div className="flex justify-between pt-6 border-t border-chorus-border-defined">
<div>
{onBack && (
<button onClick={onBack} className="btn-outline">

View File

@@ -124,7 +124,7 @@ export default function TermsAndConditions({
</div>
{/* Agreement Checkbox */}
<div className="card">
<div className="card agreement">
<div className="space-y-4">
<label className="flex items-start">
<input
@@ -152,7 +152,7 @@ export default function TermsAndConditions({
)}
{agreed && (
<div className="flex items-center text-green-600 text-sm">
<div className="flex items-center text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Thank you for accepting the terms and conditions
</div>

View File

@@ -86,14 +86,14 @@ export default function TestingValidation({
)}
{isCompleted && (
<div className="mt-8 bg-green-50 border border-green-200 rounded-lg p-6">
<h4 className="text-lg font-medium text-green-900 mb-2">
<div className="mt-8 bg-eucalyptus-50 border border-eucalyptus-950 rounded-lg p-6">
<h4 className="text-lg font-medium text-eucalyptus-600 mb-2">
🎉 Setup Complete!
</h4>
<p className="text-green-700 mb-4">
<p className="text-eucalyptus-600 mb-4">
Your CHORUS:agents cluster has been successfully configured and deployed.
</p>
<div className="space-y-2 text-sm text-green-600 mb-4">
<div className="space-y-2 text-sm text-eucalyptus-600 mb-4">
<div> System configuration validated</div>
<div> Network connectivity tested</div>
<div> Services deployed to all nodes</div>

View File

@@ -224,7 +224,7 @@ export default function SetupPage() {
<div className="grid grid-cols-1 lg:grid-cols-4 gap-12">
{/* Progress Sidebar */}
<div className="lg:col-span-1">
<div className="card sticky top-8 bg-chorus-white dark:bg-ocean-700">
<div className="card sticky top-8 setup-progress">
<h2 className="heading-subsection mb-6">
Setup Progress
</h2>
@@ -252,7 +252,7 @@ export default function SetupPage() {
<div className="flex items-center">
<div className="flex-shrink-0 mr-3">
{isCompleted ? (
<CheckCircleIcon className="h-5 w-5 text-green-400" />
<CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
) : (
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center text-xs font-medium ${
isCurrent