Fix BZZZ deployment system and deploy to ironwood

## Major Fixes:
1. **Config Download Fixed**: Frontend now sends machine_ip (snake_case) instead of machineIP (camelCase)
2. **Config Generation Fixed**: GenerateConfigForMachineSimple now provides valid whoosh_api.base_url
3. **Validation Fixed**: Deployment validation now checks for agent:, whoosh_api:, ai: (complex structure)
4. **Hardcoded Values Removed**: No more personal names/paths in deployment system

## Deployment Results:
-  Config validation passes: "Configuration loaded and validated successfully"
-  Remote deployment works: BZZZ starts in normal mode on deployed machines
-  ironwood (192.168.1.113) successfully deployed with systemd service
-  P2P networking operational with peer discovery

## Technical Details:
- Updated api/setup_manager.go: Fixed config generation and validation logic
- Updated main.go: Fixed handleDownloadConfig to return proper JSON response
- Updated ServiceDeployment.tsx: Fixed field name for API compatibility
- Added version tracking 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 21:49:05 +10:00
parent be761cfe20
commit da1b42dc33
14 changed files with 923 additions and 285 deletions

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.8

View File

@@ -1015,15 +1015,14 @@ func (s *SetupManager) executeSudoCommand(client *ssh.Client, password string, c
} }
if password != "" { if password != "" {
// SECURITY: Sanitize password to prevent breaking out of echo command // SECURITY: Use here-document to avoid password exposure in process list
safePassword := s.validator.SanitizeForCommand(password) // This keeps the password out of command line arguments and process lists
if safePassword != password { escapedPassword := strings.ReplaceAll(password, "'", "'\"'\"'")
return "", fmt.Errorf("password contains characters that could break command execution") secureCommand := fmt.Sprintf(`sudo -S %s <<'BZZZ_EOF'
} %s
BZZZ_EOF`, safeCommand, escapedPassword)
// Use password authentication with proper escaping return s.executeSSHCommand(client, secureCommand)
sudoCommand := fmt.Sprintf("echo '%s' | sudo -S %s", strings.ReplaceAll(safePassword, "'", "'\"'\"'"), safeCommand)
return s.executeSSHCommand(client, sudoCommand)
} else { } else {
// Try passwordless sudo // Try passwordless sudo
sudoCommand := fmt.Sprintf("sudo -n %s", safeCommand) sudoCommand := fmt.Sprintf("sudo -n %s", safeCommand)
@@ -1138,16 +1137,19 @@ func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config int
// Store system info for other steps to use // Store system info for other steps to use
result.SystemInfo = sysInfo result.SystemInfo = sysInfo
// Check for existing BZZZ processes // Check for existing BZZZ processes (informational only - cleanup step will handle)
output, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'No BZZZ processes found'") output, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'No BZZZ processes found'")
if err != nil { if err != nil {
s.updateLastStep(result, "failed", "process check", output, fmt.Sprintf("Failed to check processes: %v", err), false) s.updateLastStep(result, "failed", "process check", output, fmt.Sprintf("Failed to check processes: %v", err), false)
return fmt.Errorf("pre-deployment check failed: %v", err) return fmt.Errorf("pre-deployment check failed: %v", err)
} }
// Log existing processes but don't fail - cleanup step will handle this
var processStatus string
if !strings.Contains(output, "No BZZZ processes found") { if !strings.Contains(output, "No BZZZ processes found") {
s.updateLastStep(result, "failed", "", output, "Existing BZZZ processes detected - cleanup required", false) processStatus = "Existing BZZZ processes detected (will be stopped in cleanup step)"
return fmt.Errorf("existing BZZZ processes must be stopped first") } else {
processStatus = "No existing BZZZ processes detected"
} }
// Check for existing systemd services // Check for existing systemd services
@@ -1156,7 +1158,7 @@ func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config int
// Check system requirements // Check system requirements
output3, _ := s.executeSSHCommand(client, "uname -a && free -m && df -h /tmp") output3, _ := s.executeSSHCommand(client, "uname -a && free -m && df -h /tmp")
combinedOutput := fmt.Sprintf("Process check:\n%s\n\nService check:\n%s\n\nSystem info:\n%s", output, output2, output3) combinedOutput := fmt.Sprintf("Process status: %s\n\nProcess details:\n%s\n\nService check:\n%s\n\nSystem info:\n%s", processStatus, output, output2, output3)
s.updateLastStep(result, "success", "", combinedOutput, "", true) s.updateLastStep(result, "success", "", combinedOutput, "", true)
return nil return nil
} }
@@ -1170,9 +1172,13 @@ func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config i
cmd1 := "systemctl stop bzzz 2>/dev/null || echo 'No systemd service to stop'" cmd1 := "systemctl stop bzzz 2>/dev/null || echo 'No systemd service to stop'"
output1, _ := s.executeSudoCommand(client, password, cmd1) output1, _ := s.executeSudoCommand(client, password, cmd1)
// Disable and remove service file // Disable systemd service if exists - separate command for better error tracking
cmd2 := "systemctl disable bzzz 2>/dev/null; rm -f /etc/systemd/system/bzzz.service ~/.config/systemd/user/bzzz.service 2>/dev/null || echo 'No service file to remove'" cmd2a := "systemctl disable bzzz 2>/dev/null || echo 'No systemd service to disable'"
output2, _ := s.executeSudoCommand(client, password, cmd2) output2a, _ := s.executeSudoCommand(client, password, cmd2a)
// Remove service files
cmd2b := "rm -f /etc/systemd/system/bzzz.service ~/.config/systemd/user/bzzz.service 2>/dev/null || echo 'No service file to remove'"
output2b, _ := s.executeSudoCommand(client, password, cmd2b)
// Kill any remaining processes // Kill any remaining processes
cmd3 := "pkill -f bzzz || echo 'No processes to kill'" cmd3 := "pkill -f bzzz || echo 'No processes to kill'"
@@ -1189,21 +1195,21 @@ func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config i
// Verify no processes remain // Verify no processes remain
output6, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'All BZZZ processes stopped'") output6, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'All BZZZ processes stopped'")
if err != nil { if err != nil {
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2, output3, output4, output5, output6) output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "failed", "cleanup verification", combinedOutput, fmt.Sprintf("Failed verification: %v", err), false) s.updateLastStep(result, "failed", "cleanup verification", combinedOutput, fmt.Sprintf("Failed verification: %v", err), false)
return fmt.Errorf("failed to verify process cleanup: %v", err) return fmt.Errorf("failed to verify process cleanup: %v", err)
} }
if !strings.Contains(output6, "All BZZZ processes stopped") { if !strings.Contains(output6, "All BZZZ processes stopped") {
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2, output3, output4, output5, output6) output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "failed", "process verification", combinedOutput, "BZZZ processes still running after cleanup", false) s.updateLastStep(result, "failed", "process verification", combinedOutput, "BZZZ processes still running after cleanup", false)
return fmt.Errorf("failed to stop all BZZZ processes") return fmt.Errorf("failed to stop all BZZZ processes")
} }
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2, output3, output4, output5, output6) output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "success", "stop + cleanup + verify", combinedOutput, "", true) s.updateLastStep(result, "success", "stop + cleanup + verify", combinedOutput, "", true)
return nil return nil
} }
@@ -1287,11 +1293,11 @@ func (s *SetupManager) verifiedCopyBinary(client *ssh.Client, config interface{}
return fmt.Errorf("binary verification failed: %v", err) return fmt.Errorf("binary verification failed: %v", err)
} }
// Verify binary can execute and show version // Verify binary can execute (note: BZZZ doesn't have --version flag, use --help)
versionCmd := "/usr/local/bin/bzzz --version 2>/dev/null || ~/bin/bzzz --version 2>/dev/null || echo 'Version check failed'" versionCmd := "timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'"
versionOutput, _ := s.executeSSHCommand(client, versionCmd) versionOutput, _ := s.executeSSHCommand(client, versionCmd)
combinedOutput := fmt.Sprintf("File check:\n%s\n\nVersion check:\n%s", output, versionOutput) combinedOutput := fmt.Sprintf("File check:\n%s\n\nBinary test:\n%s", output, versionOutput)
if strings.Contains(output, "Binary not found") { if strings.Contains(output, "Binary not found") {
s.updateLastStep(result, "failed", checkCmd, combinedOutput, "Binary not found in expected locations", false) s.updateLastStep(result, "failed", checkCmd, combinedOutput, "Binary not found in expected locations", false)
@@ -1321,8 +1327,8 @@ func (s *SetupManager) verifiedDeployConfiguration(client *ssh.Client, config in
return fmt.Errorf("configuration verification failed: %v", err) return fmt.Errorf("configuration verification failed: %v", err)
} }
// Check if config contains expected sections // Check if config contains expected sections for complex config structure
if !strings.Contains(output, "agent:") || !strings.Contains(output, "ai:") { if !strings.Contains(output, "agent:") || !strings.Contains(output, "whoosh_api:") || !strings.Contains(output, "ai:") {
s.updateLastStep(result, "failed", verifyCmd, output, "Configuration missing required sections", false) s.updateLastStep(result, "failed", verifyCmd, output, "Configuration missing required sections", false)
return fmt.Errorf("configuration incomplete - missing required sections") return fmt.Errorf("configuration incomplete - missing required sections")
} }
@@ -1355,23 +1361,24 @@ func (s *SetupManager) verifiedCreateSystemdService(client *ssh.Client, config i
stepName := "Create SystemD Service" stepName := "Create SystemD Service"
s.addStep(result, stepName, "running", "", "", "", false) s.addStep(result, stepName, "running", "", "", "", false)
// Create systemd service using existing function // Create systemd service using password-based sudo
if err := s.createSystemdService(client, config); err != nil { if err := s.createSystemdServiceWithPassword(client, config, password); err != nil {
s.updateLastStep(result, "failed", "create service", "", err.Error(), false) s.updateLastStep(result, "failed", "create service", "", err.Error(), false)
return fmt.Errorf("systemd service creation failed: %v", err) return fmt.Errorf("systemd service creation failed: %v", err)
} }
// Verify service file was created and contains correct paths // Verify service file was created and contains correct paths
verifyCmd := "systemctl cat bzzz 2>/dev/null || echo 'Service file not found'" verifyCmd := "systemctl cat bzzz"
output, err := s.executeSudoCommand(client, password, verifyCmd) output, err := s.executeSudoCommand(client, password, verifyCmd)
if err != nil { if err != nil {
s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Service verification failed: %v", err), false) // Try to check if the service file exists another way
return fmt.Errorf("systemd service verification failed: %v", err) checkCmd := "ls -la /etc/systemd/system/bzzz.service"
} checkOutput, checkErr := s.executeSudoCommand(client, password, checkCmd)
if checkErr != nil {
if strings.Contains(output, "Service file not found") { s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Service verification failed: %v. Service file check also failed: %v", err, checkErr), false)
s.updateLastStep(result, "failed", verifyCmd, output, "SystemD service file was not created", false) return fmt.Errorf("systemd service verification failed: %v", err)
return fmt.Errorf("systemd service file creation failed") }
s.updateLastStep(result, "warning", verifyCmd, checkOutput, "Service file exists but systemctl cat failed, continuing", false)
} }
// Verify service can be enabled // Verify service can be enabled
@@ -1400,16 +1407,41 @@ func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface
return nil return nil
} }
// Pre-flight checks before starting service
s.addStep(result, "Pre-Start Checks", "running", "", "", "", false)
// Check if config file exists and is readable by the service user
configCheck := "ls -la /home/*/bzzz/config.yaml 2>/dev/null || echo 'Config file not found'"
configOutput, _ := s.executeSSHCommand(client, configCheck)
// Check if binary is executable
binCheck := "ls -la /usr/local/bin/bzzz"
binOutput, _ := s.executeSudoCommand(client, password, binCheck)
preflightInfo := fmt.Sprintf("Binary check:\n%s\n\nConfig check:\n%s", binOutput, configOutput)
s.updateLastStep(result, "success", "pre-flight", preflightInfo, "Pre-start checks completed", false)
// Start the service // Start the service
startCmd := "systemctl start bzzz" startCmd := "systemctl start bzzz"
startOutput, err := s.executeSudoCommand(client, password, startCmd) startOutput, err := s.executeSudoCommand(client, password, startCmd)
if err != nil { if err != nil {
s.updateLastStep(result, "failed", startCmd, startOutput, fmt.Sprintf("Failed to start service: %v", err), false) // Get detailed error information
statusCmd := "systemctl status bzzz"
statusOutput, _ := s.executeSudoCommand(client, password, statusCmd)
logsCmd := "journalctl -u bzzz --no-pager -n 20"
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
// Combine all error information
detailedError := fmt.Sprintf("Start command output:\n%s\n\nService status:\n%s\n\nRecent logs:\n%s",
startOutput, statusOutput, logsOutput)
s.updateLastStep(result, "failed", startCmd, detailedError, fmt.Sprintf("Failed to start service: %v", err), false)
return fmt.Errorf("failed to start systemd service: %v", err) return fmt.Errorf("failed to start systemd service: %v", err)
} }
// Wait a moment for service to start // Wait for service to fully initialize (BZZZ needs time to start all subsystems)
time.Sleep(3 * time.Second) time.Sleep(8 * time.Second)
// Verify service is running // Verify service is running
statusCmd := "systemctl status bzzz" statusCmd := "systemctl status bzzz"
@@ -1417,7 +1449,16 @@ func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface
// Check if service is active // Check if service is active
if !strings.Contains(statusOutput, "active (running)") { if !strings.Contains(statusOutput, "active (running)") {
combinedOutput := fmt.Sprintf("Start attempt:\n%s\n\nStatus check:\n%s", startOutput, statusOutput) // Get detailed logs to understand why service failed
logsCmd := "journalctl -u bzzz --no-pager -n 20"
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
// Check if config file exists and is readable
configCheckCmd := "ls -la ~/.bzzz/config.yaml && head -5 ~/.bzzz/config.yaml"
configCheckOutput, _ := s.executeSSHCommand(client, configCheckCmd)
combinedOutput := fmt.Sprintf("Start attempt:\n%s\n\nStatus check:\n%s\n\nRecent logs:\n%s\n\nConfig check:\n%s",
startOutput, statusOutput, logsOutput, configCheckOutput)
s.updateLastStep(result, "failed", startCmd, combinedOutput, "Service failed to reach running state", false) s.updateLastStep(result, "failed", startCmd, combinedOutput, "Service failed to reach running state", false)
return fmt.Errorf("service is not running after start attempt") return fmt.Errorf("service is not running after start attempt")
} }
@@ -1432,32 +1473,59 @@ func (s *SetupManager) verifiedPostDeploymentTest(client *ssh.Client, config int
stepName := "Post-deployment Test" stepName := "Post-deployment Test"
s.addStep(result, stepName, "running", "", "", "", false) s.addStep(result, stepName, "running", "", "", "", false)
// Test 1: Verify binary version // Test 1: Verify binary is executable
versionCmd := "timeout 10s /usr/local/bin/bzzz --version 2>/dev/null || timeout 10s ~/bin/bzzz --version 2>/dev/null || echo 'Version check timeout'" // Note: BZZZ binary doesn't have --version flag, so just check if it's executable and can start help
versionCmd := "if pgrep -f bzzz >/dev/null; then echo 'BZZZ process running'; else timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'; fi"
versionOutput, _ := s.executeSSHCommand(client, versionCmd) versionOutput, _ := s.executeSSHCommand(client, versionCmd)
// Test 2: Verify service status // Test 2: Verify service status
serviceCmd := "systemctl status bzzz --no-pager" serviceCmd := "systemctl status bzzz --no-pager"
serviceOutput, _ := s.executeSSHCommand(client, serviceCmd) serviceOutput, _ := s.executeSSHCommand(client, serviceCmd)
// Test 3: Check if setup API is responding (if service is running) // Test 3: Wait for API to be ready, then check if setup API is responding
apiCmd := "curl -s -m 5 http://localhost:8090/api/setup/required 2>/dev/null || echo 'API not responding'" // Poll for API readiness with timeout (up to 15 seconds)
apiOutput, _ := s.executeSSHCommand(client, apiCmd) var apiOutput string
apiReady := false
for i := 0; i < 15; i++ {
apiCmd := "curl -s -m 2 http://localhost:8090/api/setup/required 2>/dev/null"
output, err := s.executeSSHCommand(client, apiCmd)
if err == nil && !strings.Contains(output, "Connection refused") && !strings.Contains(output, "timeout") {
apiOutput = fmt.Sprintf("API ready (after %ds): %s", i+1, output)
apiReady = true
break
}
if i < 14 { // Don't sleep on the last iteration
time.Sleep(1 * time.Second)
}
}
if !apiReady {
apiOutput = "API not responding after 15s timeout"
}
// Test 4: Verify configuration is readable // Test 4: Verify configuration is readable
configCmd := "test -r ~/.bzzz/config.yaml && echo 'Config readable' || echo 'Config not readable'" configCmd := "test -r ~/.bzzz/config.yaml && echo 'Config readable' || echo 'Config not readable'"
configOutput, _ := s.executeSSHCommand(client, configCmd) configOutput, _ := s.executeSSHCommand(client, configCmd)
combinedOutput := fmt.Sprintf("Version test:\n%s\n\nService test:\n%s\n\nAPI test:\n%s\n\nConfig test:\n%s", combinedOutput := fmt.Sprintf("Binary test:\n%s\n\nService test:\n%s\n\nAPI test:\n%s\n\nConfig test:\n%s",
versionOutput, serviceOutput, apiOutput, configOutput) versionOutput, serviceOutput, apiOutput, configOutput)
// Determine if tests passed // Determine if tests passed and provide detailed failure information
testsPass := !strings.Contains(versionOutput, "Version check timeout") && // Binary test passes if BZZZ is running OR if help command succeeded
!strings.Contains(configOutput, "Config not readable") binaryFailed := strings.Contains(versionOutput, "Binary not executable") && !strings.Contains(versionOutput, "BZZZ process running")
configFailed := strings.Contains(configOutput, "Config not readable")
if !testsPass { if binaryFailed || configFailed {
s.updateLastStep(result, "failed", "post-deployment tests", combinedOutput, "One or more post-deployment tests failed", false) var failures []string
return fmt.Errorf("post-deployment verification failed") if binaryFailed {
failures = append(failures, "Binary not executable or accessible")
}
if configFailed {
failures = append(failures, "Config file not readable")
}
failureMsg := fmt.Sprintf("Tests failed: %s", strings.Join(failures, ", "))
s.updateLastStep(result, "failed", "post-deployment tests", combinedOutput, failureMsg, false)
return fmt.Errorf("post-deployment verification failed: %s", failureMsg)
} }
s.updateLastStep(result, "success", "comprehensive verification", combinedOutput, "", true) s.updateLastStep(result, "success", "comprehensive verification", combinedOutput, "", true)
@@ -1588,6 +1656,87 @@ func (s *SetupManager) copyBinaryToMachine(client *ssh.Client) error {
return s.copyBinaryToMachineWithPassword(client, "") return s.copyBinaryToMachineWithPassword(client, "")
} }
// createSystemdServiceWithPassword creates systemd service file using password sudo
func (s *SetupManager) createSystemdServiceWithPassword(client *ssh.Client, config interface{}, password string) error {
// Determine the correct binary path
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
// Check where the binary was installed
binaryPath := "/usr/local/bin/bzzz"
if err := session.Run("test -f /usr/local/bin/bzzz"); err != nil {
// If not in /usr/local/bin, it should be in ~/bin
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Stdout = &stdout
if err := session.Run("echo $HOME/bin/bzzz"); err == nil {
binaryPath = strings.TrimSpace(stdout.String())
}
}
// Get the actual username for the service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
var userBuilder strings.Builder
session.Stdout = &userBuilder
if err := session.Run("whoami"); err != nil {
return fmt.Errorf("failed to get username: %w", err)
}
username := strings.TrimSpace(userBuilder.String())
// Create service file with actual username
serviceFile := fmt.Sprintf(`[Unit]
Description=BZZZ P2P Task Coordination System
Documentation=https://chorus.services/docs/bzzz
After=network.target
[Service]
Type=simple
ExecStart=%s --config /home/%s/.bzzz/config.yaml
Restart=always
RestartSec=10
User=%s
Group=%s
[Install]
WantedBy=multi-user.target
`, binaryPath, username, username, username)
// Create service file in temp location first, then move with sudo
createCmd := fmt.Sprintf("cat > /tmp/bzzz.service << 'EOF'\n%sEOF", serviceFile)
if _, err := s.executeSSHCommand(client, createCmd); err != nil {
return fmt.Errorf("failed to create temp service file: %w", err)
}
// Move to systemd directory using password sudo
moveCmd := "mv /tmp/bzzz.service /etc/systemd/system/bzzz.service"
if _, err := s.executeSudoCommand(client, password, moveCmd); err != nil {
return fmt.Errorf("failed to install system service file: %w", err)
}
// Reload systemd to recognize new service
reloadCmd := "systemctl daemon-reload"
if _, err := s.executeSudoCommand(client, password, reloadCmd); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
return nil
}
// createSystemdService creates systemd service file // createSystemdService creates systemd service file
func (s *SetupManager) createSystemdService(client *ssh.Client, config interface{}) error { func (s *SetupManager) createSystemdService(client *ssh.Client, config interface{}) error {
// Determine the correct binary path // Determine the correct binary path
@@ -1747,28 +1896,16 @@ func (s *SetupManager) startService(client *ssh.Client) error {
return nil return nil
} }
// generateAndDeployConfig generates node-specific config.yaml and deploys it // GenerateConfigForMachine generates the YAML configuration for a specific machine (for download/inspection)
func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error { func (s *SetupManager) GenerateConfigForMachine(machineIP string, config interface{}) (string, error) {
// Extract configuration from the setup data // Extract configuration from the setup data
configMap, ok := config.(map[string]interface{}) configMap, ok := config.(map[string]interface{})
if !ok { if !ok {
// Log the actual type and value for debugging return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config)
return fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config)
} }
// Get hostname for unique agent ID // Use machine IP to determine hostname (simplified)
session, err := client.NewSession() hostname := strings.ReplaceAll(machineIP, ".", "-")
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
if err := session.Run("hostname"); err != nil {
return fmt.Errorf("failed to get hostname: %w", err)
}
hostname := strings.TrimSpace(stdout.String())
// Extract ports from configuration // Extract ports from configuration
ports := map[string]interface{}{ ports := map[string]interface{}{
@@ -1800,61 +1937,321 @@ func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string
} }
} }
// Generate YAML configuration // Generate YAML configuration that matches the Go struct layout
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s configYAML := fmt.Sprintf(`# BZZZ Configuration for %s
agent:
id: "%s-agent"
name: "%s Agent"
specialization: "general_developer"
capabilities: ["general", "reasoning", "task-coordination"]
models: ["phi3", "llama3.1"]
max_tasks: 3
# AI/LLM configuration
ai:
ollama:
base_url: "http://192.168.1.27:11434"
timeout: 30s
# Network configuration
network:
listen_ip: "0.0.0.0"
ports:
api: %v
mcp: %v
webui: %v
p2p: %v
# Security configuration
security:
cluster_secret: "%v"
audit_logging: true
key_rotation_days: 90
# Storage configuration
storage:
data_dir: "~/.bzzz/data"
# Logging configuration
logging:
level: "info"
file: "~/.bzzz/logs/bzzz.log"
# GitHub integration (optional)
github:
token_file: "/home/tony/chorus/business/secrets/gh-token"
timeout: 30s
# WHOOSH API configuration
whoosh_api: whoosh_api:
base_url: "https://hive.home.deepblack.cloud" base_url: "https://whoosh.home.deepblack.cloud"
timeout: 30s timeout: 30s
retry_count: 3 retry_count: 3
# P2P configuration agent:
id: "%s-agent"
capabilities: ["general", "reasoning", "task-coordination"]
poll_interval: 30s
max_tasks: 3
models: ["phi3", "llama3.1"]
specialization: "general_developer"
model_selection_webhook: "https://n8n.home.deepblack.cloud/webhook/model-selection"
default_reasoning_model: "phi3"
sandbox_image: "registry.home.deepblack.cloud/bzzz-sandbox:latest"
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "Bzzz-P2P-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p: p2p:
escalation_webhook: "https://n8n.home.deepblack.cloud/webhook/escalation" service_tag: "bzzz-peer-discovery"
`, hostname, hostname, hostname, ports["api"], ports["mcp"], ports["webui"], ports["p2p"], securityConfig["cluster_secret"]) bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: "https://n8n.home.deepblack.cloud/webhook-test/human-escalation"
escalation_keywords: ["stuck", "help", "human", "escalate", "clarification needed", "manual intervention"]
conversation_limit: 10
logging:
level: "info"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: true
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: true
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "/tmp/bzzz-ucxl-storage"
max_size: 104857600
p2p_integration:
enable_announcement: true
enable_discovery: true
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: true
audit_path: ".bzzz/security-audit.log"
ai:
ollama:
endpoint: "http://192.168.1.27:11434"
timeout: 30s
models: ["phi3", "llama3.1"]
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"
timeout: 30s
`, hostname, hostname)
return configYAML, nil
}
// GenerateConfigForMachineSimple generates a simple BZZZ configuration that matches the working config structure
func (s *SetupManager) GenerateConfigForMachineSimple(machineIP string, config interface{}) (string, error) {
// Note: Configuration extraction not needed for minimal template
_ = config // Avoid unused parameter warning
// Use machine IP to determine hostname (simplified)
hostname := strings.ReplaceAll(machineIP, ".", "-")
// Note: Using minimal config template - ports and security can be configured later
// Generate YAML configuration that matches the Go struct requirements (minimal valid config)
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s
whoosh_api:
base_url: "https://whoosh.home.deepblack.cloud"
api_key: ""
timeout: 30s
retry_count: 3
agent:
id: "%s-agent"
capabilities: ["general"]
poll_interval: 30s
max_tasks: 2
models: []
specialization: ""
model_selection_webhook: ""
default_reasoning_model: ""
sandbox_image: ""
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "BZZZ-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p:
service_tag: "bzzz-peer-discovery"
bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: ""
escalation_keywords: []
conversation_limit: 10
logging:
level: "info"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: false
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: false
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "/tmp/bzzz-ucxl-storage"
max_size: 104857600
p2p_integration:
enable_announcement: false
enable_discovery: false
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: false
audit_path: ""
ai:
ollama:
endpoint: ""
timeout: 30s
models: []
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"
timeout: 30s
`, hostname, hostname)
return configYAML, nil
}
// generateAndDeployConfig generates node-specific config.yaml and deploys it
func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error {
// Get hostname for unique agent ID
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
if err := session.Run("hostname"); err != nil {
return fmt.Errorf("failed to get hostname: %w", err)
}
hostname := strings.TrimSpace(stdout.String())
// Generate YAML configuration using the shared method
configYAML, err := s.GenerateConfigForMachineSimple(hostname, config)
if err != nil {
return fmt.Errorf("failed to generate config: %w", err)
}
// Create configuration directory // Create configuration directory
session, err = client.NewSession() session, err = client.NewSession()

View File

@@ -14,7 +14,8 @@ import {
CloudArrowDownIcon, CloudArrowDownIcon,
Cog6ToothIcon, Cog6ToothIcon,
XMarkIcon, XMarkIcon,
ComputerDesktopIcon ComputerDesktopIcon,
ArrowDownTrayIcon
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
interface Machine { interface Machine {
@@ -303,9 +304,10 @@ export default function ServiceDeployment({
// Show actual backend steps if provided // Show actual backend steps if provided
if (result.steps) { if (result.steps) {
result.steps.forEach((step: string) => { result.steps.forEach((step: any) => {
logs.push(step) const stepText = `${step.name}: ${step.status}${step.error ? ` - ${step.error}` : ''}${step.duration ? ` (${step.duration})` : ''}`
addConsoleLog(`📋 ${step}`) logs.push(stepText)
addConsoleLog(`📋 ${stepText}`)
}) })
} }
addConsoleLog(`🎉 CHORUS:agents service is now running on ${machine?.hostname}`) addConsoleLog(`🎉 CHORUS:agents service is now running on ${machine?.hostname}`)
@@ -378,6 +380,50 @@ export default function ServiceDeployment({
}) })
} }
const downloadConfig = async (machineId: string) => {
try {
const machine = machines.find(m => m.id === machineId)
if (!machine) return
const response = await fetch('/api/setup/download-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
machine_ip: machine.ip,
config: {
ports: {
api: configData?.network?.bzzzPort || 8080,
mcp: configData?.network?.mcpPort || 3000,
webui: configData?.network?.webUIPort || 8080,
p2p: configData?.network?.p2pPort || 7000
},
security: configData?.security,
autoStart: config.autoStart
}
})
})
if (response.ok) {
const result = await response.json()
// Create blob and download
const blob = new Blob([result.configYAML], { type: 'text/yaml' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `bzzz-config-${machine.hostname}-${machine.ip}.yaml`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} else {
console.error('Failed to download config:', await response.text())
}
} catch (error) {
console.error('Config download error:', error)
}
}
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
case 'connected': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" /> case 'connected': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
@@ -481,36 +527,31 @@ export default function ServiceDeployment({
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Select <span className="sr-only sm:not-sr-only">Select</span>
<span className="sm:hidden">✓</span>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Machine Machine / Connection
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3 hidden md:table-cell">
Operating System Operating System
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
IP Address
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
SSH Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Deploy Status Deploy Status
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Actions Actions
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-1 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-2 sm:py-3">
Remove <span className="sr-only">Remove</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{machines.map((machine) => ( {machines.map((machine) => (
<tr key={machine.id} className={machine.selected ? 'bg-blue-50' : ''}> <tr key={machine.id} className={machine.selected ? 'bg-blue-50' : ''}>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<input <input
type="checkbox" type="checkbox"
checked={machine.selected} checked={machine.selected}
@@ -518,118 +559,130 @@ export default function ServiceDeployment({
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded" className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/> />
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<div> <div>
<div className="text-sm font-medium text-gray-900">{machine.hostname}</div> <div className="text-sm font-medium text-gray-900">{machine.hostname}</div>
{machine.systemInfo && ( <div className="text-xs text-gray-500 space-y-1">
<div className="text-xs text-gray-500"> <div className="inline-flex items-center space-x-2">
{machine.systemInfo.cpu} cores • {machine.systemInfo.memory}GB RAM • {machine.systemInfo.disk}GB disk <span>{machine.ip}</span>
<span className="inline-flex items-center" title={`SSH Status: ${machine.sshStatus.replace('_', ' ')}`}>
{getStatusIcon(machine.sshStatus)}
</span>
</div> </div>
)} {machine.systemInfo && (
</div> <div className="text-gray-400">
</td> {machine.systemInfo.cpu}c • {machine.systemInfo.memory}GB • {machine.systemInfo.disk}GB
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{machine.os}</div>
<div className="text-xs text-gray-500">{machine.osVersion}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{machine.ip}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getStatusIcon(machine.sshStatus)}
<span className="ml-2 text-sm text-gray-900 capitalize">
{machine.sshStatus.replace('_', ' ')}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getStatusIcon(machine.deployStatus)}
<div className="ml-2 flex-1">
<div className="text-sm text-gray-900 capitalize">
{machine.deployStatus.replace('_', ' ')}
</div>
{machine.deployStatus === 'installing' && (
<div className="mt-1">
<div className="text-xs text-gray-500 mb-1">
{machine.deployStep || 'Deploying...'}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${machine.deployProgress || 0}%` }}
/>
</div>
<div className="text-xs text-gray-500 mt-1">
{machine.deployProgress || 0}%
</div>
</div> </div>
)} )}
</div> </div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2"> <td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3 hidden md:table-cell">
{machine.id !== 'localhost' && machine.sshStatus !== 'connected' && ( <div className="text-sm text-gray-900">{machine.os}</div>
<button <div className="text-xs text-gray-500">{machine.osVersion}</div>
type="button"
onClick={() => testSSHConnection(machine.id)}
className="text-blue-600 hover:text-blue-900"
disabled={machine.sshStatus === 'testing'}
>
Test SSH
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
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
type="button"
onClick={() => setShowLogs(machine.id)}
className="text-gray-600 hover:text-gray-900 mr-2"
title="View deployment logs"
>
<DocumentTextIcon className="h-4 w-4 inline" />
</button>
<button
type="button"
onClick={() => setShowConsole(machine.id)}
className="text-blue-600 hover:text-blue-900"
title="Open deployment console"
>
<ComputerDesktopIcon className="h-4 w-4 inline" />
</button>
</>
)}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<div className="flex items-center">
<div className="inline-flex items-center" title={`Deploy Status: ${machine.deployStatus.replace('_', ' ')}`}>
{getStatusIcon(machine.deployStatus)}
</div>
{machine.deployStatus === 'installing' && (
<div className="ml-2 flex-1">
<div className="text-xs text-gray-500 mb-1 truncate">
{machine.deployStep || 'Deploying...'}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${machine.deployProgress || 0}%` }}
/>
</div>
<div className="text-xs text-gray-500 mt-1">
{machine.deployProgress || 0}%
</div>
</div>
)}
</div>
</td>
<td className="px-2 py-2 whitespace-nowrap text-sm font-medium sm:px-4 sm:py-3">
<div className="flex flex-wrap gap-1">
{machine.id !== 'localhost' && machine.sshStatus !== 'connected' && (
<button
type="button"
onClick={() => testSSHConnection(machine.id)}
className="text-blue-600 hover:text-blue-700 text-xs px-2 py-1 bg-blue-50 rounded"
disabled={machine.sshStatus === 'testing'}
title="Test SSH connection"
>
Test SSH
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-eucalyptus-600 hover:text-eucalyptus-700 text-xs px-2 py-1 bg-eucalyptus-50 rounded"
title="Deploy BZZZ"
>
Install
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'error' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-amber-600 hover:text-amber-700 text-xs px-2 py-1 bg-amber-50 rounded inline-flex items-center"
title="Retry deployment"
>
<ArrowPathIcon className="h-3 w-3 mr-1" />
Retry
</button>
)}
{machine.sshStatus === 'connected' && (
<button
type="button"
onClick={() => downloadConfig(machine.id)}
className="text-purple-600 hover:text-purple-700 text-xs px-2 py-1 bg-purple-50 rounded inline-flex items-center"
title="Download configuration file"
>
<ArrowDownTrayIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Config</span>
</button>
)}
{machine.deployStatus !== 'not_deployed' && (
<>
<button
type="button"
onClick={() => setShowLogs(machine.id)}
className="text-gray-600 hover:text-gray-700 text-xs px-2 py-1 bg-gray-50 rounded inline-flex items-center"
title="View deployment logs"
>
<DocumentTextIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Logs</span>
</button>
<button
type="button"
onClick={() => setShowConsole(machine.id)}
className="text-blue-600 hover:text-blue-700 text-xs px-2 py-1 bg-blue-50 rounded inline-flex items-center"
title="Open deployment console"
>
<ComputerDesktopIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Console</span>
</button>
</>
)}
</div>
</td>
<td className="px-1 py-2 whitespace-nowrap text-sm font-medium sm:px-2 sm:py-3">
{machine.id !== 'localhost' && ( {machine.id !== 'localhost' && (
<button <button
type="button" type="button"
onClick={() => removeMachine(machine.id)} onClick={() => removeMachine(machine.id)}
className="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50" className="text-red-600 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove machine" title="Remove machine"
> >
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />

137
ironwood-config.yaml Normal file
View File

@@ -0,0 +1,137 @@
# BZZZ Configuration for 192-168-1-113
whoosh_api:
base_url: "https://whoosh.home.deepblack.cloud"
api_key: ""
timeout: 30s
retry_count: 3
agent:
id: "192-168-1-113-agent"
capabilities: ["general"]
poll_interval: 30s
max_tasks: 2
models: []
specialization: ""
model_selection_webhook: ""
default_reasoning_model: ""
sandbox_image: ""
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "BZZZ-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p:
service_tag: "bzzz-peer-discovery"
bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: ""
escalation_keywords: []
conversation_limit: 10
logging:
level: "info"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: false
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: false
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "/tmp/bzzz-ucxl-storage"
max_size: 104857600
p2p_integration:
enable_announcement: false
enable_discovery: false
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: false
audit_path: ""
ai:
ollama:
endpoint: "http://192.168.1.113:11434"
timeout: 30s
models: []
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"
timeout: 30s

67
main.go
View File

@@ -11,6 +11,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"time" "time"
"chorus.services/bzzz/api" "chorus.services/bzzz/api"
@@ -20,6 +21,7 @@ import (
"chorus.services/bzzz/p2p" "chorus.services/bzzz/p2p"
"chorus.services/bzzz/pkg/config" "chorus.services/bzzz/pkg/config"
"chorus.services/bzzz/pkg/crypto" "chorus.services/bzzz/pkg/crypto"
"chorus.services/bzzz/pkg/version"
"chorus.services/bzzz/pkg/dht" "chorus.services/bzzz/pkg/dht"
"chorus.services/bzzz/pkg/election" "chorus.services/bzzz/pkg/election"
"chorus.services/bzzz/pkg/health" "chorus.services/bzzz/pkg/health"
@@ -105,12 +107,22 @@ func main() {
// Parse command line arguments // Parse command line arguments
var configPath string var configPath string
var setupMode bool var setupMode bool
var showVersion bool
flag.StringVar(&configPath, "config", "", "Path to configuration file") flag.StringVar(&configPath, "config", "", "Path to configuration file")
flag.BoolVar(&setupMode, "setup", false, "Start in setup mode") flag.BoolVar(&setupMode, "setup", false, "Start in setup mode")
flag.BoolVar(&showVersion, "version", false, "Show version information")
flag.Parse() flag.Parse()
fmt.Println("🚀 Starting Bzzz v1.0.2 + HMMM P2P Task Coordination System...") // Handle version flag
if showVersion {
fmt.Printf("BZZZ %s\n", version.FullVersion())
fmt.Printf("Build Date: %s\n", time.Now().Format("2006-01-02"))
fmt.Printf("Go Version: %s\n", runtime.Version())
return
}
fmt.Printf("🚀 Starting Bzzz %s + HMMM P2P Task Coordination System...\n", version.FullVersion())
// Determine config file path with priority order: // Determine config file path with priority order:
// 1. Command line argument // 1. Command line argument
@@ -515,7 +527,7 @@ func main() {
shutdownManager := shutdown.NewManager(30*time.Second, &simpleLogger{}) shutdownManager := shutdown.NewManager(30*time.Second, &simpleLogger{})
// Initialize health manager // Initialize health manager
healthManager := health.NewManager(node.ID().ShortString(), "v0.2.0", &simpleLogger{}) healthManager := health.NewManager(node.ID().ShortString(), version.FullVersion(), &simpleLogger{})
healthManager.SetShutdownManager(shutdownManager) healthManager.SetShutdownManager(shutdownManager)
// Register health checks // Register health checks
@@ -936,7 +948,7 @@ func announceCapabilitiesOnChange(ps *pubsub.PubSub, nodeID string, cfg *config.
"node_id": nodeID, "node_id": nodeID,
"capabilities": cfg.Agent.Capabilities, "capabilities": cfg.Agent.Capabilities,
"models": cfg.Agent.Models, "models": cfg.Agent.Models,
"version": "0.2.0", "version": version.Version(),
"specialization": cfg.Agent.Specialization, "specialization": cfg.Agent.Specialization,
} }
@@ -1246,6 +1258,8 @@ func startSetupMode(configPath string) {
http.HandleFunc("/api/setup/discover-machines", corsHandler(handleDiscoverMachines(setupManager))) http.HandleFunc("/api/setup/discover-machines", corsHandler(handleDiscoverMachines(setupManager)))
http.HandleFunc("/api/setup/test-ssh", corsHandler(handleTestSSH(setupManager))) http.HandleFunc("/api/setup/test-ssh", corsHandler(handleTestSSH(setupManager)))
http.HandleFunc("/api/setup/deploy-service", corsHandler(handleDeployService(setupManager))) http.HandleFunc("/api/setup/deploy-service", corsHandler(handleDeployService(setupManager)))
http.HandleFunc("/api/setup/download-config", corsHandler(handleDownloadConfig(setupManager)))
http.HandleFunc("/api/version", corsHandler(handleVersion()))
http.HandleFunc("/api/health", corsHandler(handleSetupHealth)) http.HandleFunc("/api/health", corsHandler(handleSetupHealth))
fmt.Printf("🎯 Setup interface available at: http://localhost:8090\n") fmt.Printf("🎯 Setup interface available at: http://localhost:8090\n")
@@ -1320,6 +1334,53 @@ func handleSetupRequired(sm *api.SetupManager) http.HandlerFunc {
} }
} }
func handleVersion() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"version": version.Version(),
"full_version": version.FullVersion(),
"timestamp": time.Now().Unix(),
}
json.NewEncoder(w).Encode(response)
}
}
func handleDownloadConfig(sm *api.SetupManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
MachineIP string `json:"machine_ip"`
Config map[string]interface{} `json:"config"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Generate the same config that would be deployed
configYAML, err := sm.GenerateConfigForMachineSimple(req.MachineIP, req.Config)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to generate config: %v", err), http.StatusInternalServerError)
return
}
// Return JSON response with YAML content
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"success": true,
"configYAML": configYAML,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
}
func handleSystemDetection(sm *api.SetupManager) http.HandlerFunc { func handleSystemDetection(sm *api.SetupManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@@ -45,12 +45,11 @@ type OpenAIConfig struct {
// Config represents the complete configuration for a Bzzz agent // Config represents the complete configuration for a Bzzz agent
type Config struct { type Config struct {
WHOOSHAPI WHOOSHAPIConfig `yaml:"hive_api"` WHOOSHAPI WHOOSHAPIConfig `yaml:"whoosh_api"`
Agent AgentConfig `yaml:"agent"` Agent AgentConfig `yaml:"agent"`
GitHub GitHubConfig `yaml:"github"` GitHub GitHubConfig `yaml:"github"`
P2P P2PConfig `yaml:"p2p"` P2P P2PConfig `yaml:"p2p"`
Logging LoggingConfig `yaml:"logging"` Logging LoggingConfig `yaml:"logging"`
HCFS HCFSConfig `yaml:"hcfs"`
Slurp SlurpConfig `yaml:"slurp"` Slurp SlurpConfig `yaml:"slurp"`
V2 V2Config `yaml:"v2"` // BZZZ v2 protocol settings V2 V2Config `yaml:"v2"` // BZZZ v2 protocol settings
UCXL UCXLConfig `yaml:"ucxl"` // UCXL protocol settings UCXL UCXLConfig `yaml:"ucxl"` // UCXL protocol settings
@@ -226,31 +225,6 @@ type UCXLP2PConfig struct {
DiscoveryTimeout time.Duration `yaml:"discovery_timeout" json:"discovery_timeout"` DiscoveryTimeout time.Duration `yaml:"discovery_timeout" json:"discovery_timeout"`
} }
// HCFSConfig holds HCFS integration configuration
type HCFSConfig struct {
// API settings
APIURL string `yaml:"api_url" json:"api_url"`
APITimeout time.Duration `yaml:"api_timeout" json:"api_timeout"`
// Workspace settings
MountPath string `yaml:"mount_path" json:"mount_path"`
WorkspaceTimeout time.Duration `yaml:"workspace_timeout" json:"workspace_timeout"`
// FUSE settings
FUSEEnabled bool `yaml:"fuse_enabled" json:"fuse_enabled"`
FUSEMountPoint string `yaml:"fuse_mount_point" json:"fuse_mount_point"`
// Cleanup settings
IdleCleanupInterval time.Duration `yaml:"idle_cleanup_interval" json:"idle_cleanup_interval"`
MaxIdleTime time.Duration `yaml:"max_idle_time" json:"max_idle_time"`
// Storage settings
StoreArtifacts bool `yaml:"store_artifacts" json:"store_artifacts"`
CompressArtifacts bool `yaml:"compress_artifacts" json:"compress_artifacts"`
// Enable/disable HCFS integration
Enabled bool `yaml:"enabled" json:"enabled"`
}
// LoadConfig loads configuration from file, environment variables, and defaults // LoadConfig loads configuration from file, environment variables, and defaults
func LoadConfig(configPath string) (*Config, error) { func LoadConfig(configPath string) (*Config, error) {
@@ -281,7 +255,7 @@ func LoadConfig(configPath string) (*Config, error) {
func getDefaultConfig() *Config { func getDefaultConfig() *Config {
return &Config{ return &Config{
WHOOSHAPI: WHOOSHAPIConfig{ WHOOSHAPI: WHOOSHAPIConfig{
BaseURL: "https://hive.home.deepblack.cloud", BaseURL: "https://whoosh.home.deepblack.cloud",
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
RetryCount: 3, RetryCount: 3,
}, },
@@ -317,19 +291,6 @@ func getDefaultConfig() *Config {
Output: "stdout", Output: "stdout",
Structured: false, Structured: false,
}, },
HCFS: HCFSConfig{
APIURL: "http://localhost:8000",
APITimeout: 30 * time.Second,
MountPath: "/tmp/hcfs-workspaces",
WorkspaceTimeout: 2 * time.Hour,
FUSEEnabled: false,
FUSEMountPoint: "/mnt/hcfs",
IdleCleanupInterval: 15 * time.Minute,
MaxIdleTime: 1 * time.Hour,
StoreArtifacts: true,
CompressArtifacts: false,
Enabled: true,
},
Slurp: GetDefaultSlurpConfig(), Slurp: GetDefaultSlurpConfig(),
UCXL: UCXLConfig{ UCXL: UCXLConfig{
Enabled: false, // Disabled by default Enabled: false, // Disabled by default
@@ -456,10 +417,10 @@ func loadFromFile(config *Config, filePath string) error {
// loadFromEnv loads configuration from environment variables // loadFromEnv loads configuration from environment variables
func loadFromEnv(config *Config) error { func loadFromEnv(config *Config) error {
// WHOOSH API configuration // WHOOSH API configuration
if url := os.Getenv("BZZZ_HIVE_API_URL"); url != "" { if url := os.Getenv("BZZZ_WHOOSH_API_URL"); url != "" {
config.WHOOSHAPI.BaseURL = url config.WHOOSHAPI.BaseURL = url
} }
if apiKey := os.Getenv("BZZZ_HIVE_API_KEY"); apiKey != "" { if apiKey := os.Getenv("BZZZ_WHOOSH_API_KEY"); apiKey != "" {
config.WHOOSHAPI.APIKey = apiKey config.WHOOSHAPI.APIKey = apiKey
} }
@@ -533,7 +494,7 @@ func loadFromEnv(config *Config) error {
func validateConfig(config *Config) error { func validateConfig(config *Config) error {
// Validate required fields // Validate required fields
if config.WHOOSHAPI.BaseURL == "" { if config.WHOOSHAPI.BaseURL == "" {
return fmt.Errorf("hive_api.base_url is required") return fmt.Errorf("whoosh_api.base_url is required")
} }
// Note: Agent.ID can be empty - it will be auto-generated from node ID in main.go // Note: Agent.ID can be empty - it will be auto-generated from node ID in main.go

1
pkg/version/VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.8

19
pkg/version/version.go Normal file
View File

@@ -0,0 +1,19 @@
package version
import (
_ "embed"
"strings"
)
//go:embed VERSION
var versionContent string
// Version returns the current BZZZ version
func Version() string {
return strings.TrimSpace(versionContent)
}
// FullVersion returns a formatted version string
func FullVersion() string {
return "v" + Version()
}

View File

@@ -0,0 +1 @@
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-e87e5963ec1b8011.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View File

@@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{1380:function(e,t,r){Promise.resolve().then(r.bind(r,9174)),Promise.resolve().then(r.bind(r,2724)),Promise.resolve().then(r.t.bind(r,2445,23))},9174:function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return l}});var n=r(7437),s=r(2265);let a=s.forwardRef(function({title:e,titleId:t,...r},n){return s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:n,"aria-labelledby":t},r),e?s.createElement("title",{id:t},e):null,s.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"}))}),o=s.forwardRef(function({title:e,titleId:t,...r},n){return s.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:n,"aria-labelledby":t},r),e?s.createElement("title",{id:t},e):null,s.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"}))});function l(){let[e,t]=(0,s.useState)(!0);(0,s.useEffect)(()=>{let e=localStorage.getItem("chorus-theme"),n=!e||"dark"===e;t(n),r(n)},[]);let r=e=>{let t=document.documentElement;e?(t.classList.add("dark"),t.classList.remove("light")):(t.classList.remove("dark"),t.classList.add("light"))};return(0,n.jsx)("button",{onClick:()=>{let n=!e;t(n),r(n),localStorage.setItem("chorus-theme",n?"dark":"light")},className:"btn-text flex items-center space-x-2 p-2 rounded-md transition-colors duration-200","aria-label":"Switch to ".concat(e?"light":"dark"," theme"),children:e?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(a,{className:"h-4 w-4"}),(0,n.jsx)("span",{className:"text-xs",children:"Light"})]}):(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o,{className:"h-4 w-4"}),(0,n.jsx)("span",{className:"text-xs",children:"Dark"})]})})}},2724:function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return a}});var n=r(7437),s=r(2265);function a(){let[e,t]=(0,s.useState)(null);return((0,s.useEffect)(()=>{(async()=>{try{let e=await fetch("/api/version");if(e.ok){let r=await e.json();t(r)}}catch(e){console.warn("Failed to fetch version:",e)}})()},[]),e)?(0,n.jsxs)("div",{className:"text-xs text-gray-500",children:["BZZZ ",e.full_version]}):(0,n.jsx)("div",{className:"text-xs text-gray-500",children:"BZZZ"})}},2445:function(){},622:function(e,t,r){"use strict";var n=r(2265),s=Symbol.for("react.element"),a=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,l=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!i.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:s,type:e,key:c,ref:u,props:a,_owner:l.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},7437:function(e,t,r){"use strict";e.exports=r(622)}},function(e){e.O(0,[971,938,744],function(){return e(e.s=1380)}),_N_E=e.O()}]);

View File

@@ -0,0 +1 @@
!function(){"use strict";var e,t,r,n,o,u,i,c,f,a={},l={};function s(e){var t=l[e];if(void 0!==t)return t.exports;var r=l[e]={exports:{}},n=!0;try{a[e](r,r.exports,s),n=!1}finally{n&&delete l[e]}return r.exports}s.m=a,e=[],s.O=function(t,r,n,o){if(r){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[r,n,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var r=e[u][0],n=e[u][1],o=e[u][2],c=!0,f=0;f<r.length;f++)i>=o&&Object.keys(s.O).every(function(e){return s.O[e](r[f])})?r.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=n();void 0!==a&&(t=a)}}return t},r=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},s.t=function(e,n){if(1&n&&(e=this(e)),8&n||"object"==typeof e&&e&&(4&n&&e.__esModule||16&n&&"function"==typeof e.then))return e;var o=Object.create(null);s.r(o);var u={};t=t||[null,r({}),r([]),r(r)];for(var i=2&n&&e;"object"==typeof i&&!~t.indexOf(i);i=r(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},s.d(o,u),o},s.d=function(e,t){for(var r in t)s.o(t,r)&&!s.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},s.f={},s.e=function(e){return Promise.all(Object.keys(s.f).reduce(function(t,r){return s.f[r](e,t),t},[]))},s.u=function(e){},s.miniCssF=function(e){return"static/css/7a9299e2c7bea835.css"},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n={},o="_N_E:",s.l=function(e,t,r,u){if(n[e]){n[e].push(t);return}if(void 0!==r)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+r){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,s.nc&&i.setAttribute("nonce",s.nc),i.setAttribute("data-webpack",o+r),i.src=s.tu(e)),n[e]=[t];var d=function(t,r){i.onerror=i.onload=null,clearTimeout(p);var o=n[e];if(delete n[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(r)}),t)return t(r)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=d.bind(null,i.onerror),i.onload=d.bind(null,i.onload),c&&document.head.appendChild(i)},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},s.tu=function(e){return s.tt().createScriptURL(e)},s.p="/setup/_next/",i={272:0},s.f.j=function(e,t){var r=s.o(i,e)?i[e]:void 0;if(0!==r){if(r)t.push(r[2]);else if(272!=e){var n=new Promise(function(t,n){r=i[e]=[t,n]});t.push(r[2]=n);var o=s.p+s.u(e),u=Error();s.l(o,function(t){if(s.o(i,e)&&(0!==(r=i[e])&&(i[e]=void 0),r)){var n=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+n+": "+o+")",u.name="ChunkLoadError",u.type=n,u.request=o,r[1](u)}},"chunk-"+e,e)}else i[e]=0}},s.O.j=function(e){return 0===i[e]},c=function(e,t){var r,n,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(r in u)s.o(u,r)&&(s.m[r]=u[r]);if(c)var a=c(s)}for(e&&e(t);f<o.length;f++)n=o[f],s.o(i,n)&&i[n]&&i[n][0](),i[n]=0;return s.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();

File diff suppressed because one or more lines are too long