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:
310
internal/common/runtime/task_tracker.go
Normal file
310
internal/common/runtime/task_tracker.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"chorus.services/bzzz/pkg/ucxl"
|
||||
"chorus.services/bzzz/pubsub"
|
||||
)
|
||||
|
||||
// TaskTracker implements the SimpleTaskTracker interface
|
||||
type TaskTracker struct {
|
||||
maxTasks int
|
||||
activeTasks map[string]bool
|
||||
decisionPublisher *ucxl.DecisionPublisher
|
||||
pubsub *pubsub.PubSub
|
||||
nodeID string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTaskTracker creates a new task tracker
|
||||
func NewTaskTracker(maxTasks int, nodeID string, ps *pubsub.PubSub) SimpleTaskTracker {
|
||||
return &TaskTracker{
|
||||
maxTasks: maxTasks,
|
||||
activeTasks: make(map[string]bool),
|
||||
pubsub: ps,
|
||||
nodeID: nodeID,
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveTasks returns list of active task IDs
|
||||
func (t *TaskTracker) GetActiveTasks() []string {
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
tasks := make([]string, 0, len(t.activeTasks))
|
||||
for taskID := range t.activeTasks {
|
||||
tasks = append(tasks, taskID)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
// GetMaxTasks returns maximum number of concurrent tasks
|
||||
func (t *TaskTracker) GetMaxTasks() int {
|
||||
return t.maxTasks
|
||||
}
|
||||
|
||||
// AddTask marks a task as active
|
||||
func (t *TaskTracker) AddTask(taskID string) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
t.activeTasks[taskID] = true
|
||||
}
|
||||
|
||||
// RemoveTask marks a task as completed and publishes decision if publisher available
|
||||
func (t *TaskTracker) RemoveTask(taskID string) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
delete(t.activeTasks, taskID)
|
||||
|
||||
// Publish task completion decision if publisher is available
|
||||
if t.decisionPublisher != nil {
|
||||
go t.publishTaskCompletion(taskID, true, "Task completed successfully", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteTaskWithDecision marks a task as completed and publishes detailed decision
|
||||
func (t *TaskTracker) CompleteTaskWithDecision(taskID string, success bool, summary string, filesModified []string) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
delete(t.activeTasks, taskID)
|
||||
|
||||
// Publish task completion decision if publisher is available
|
||||
if t.decisionPublisher != nil {
|
||||
go t.publishTaskCompletion(taskID, success, summary, filesModified)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDecisionPublisher sets the decision publisher for task completion tracking
|
||||
func (t *TaskTracker) SetDecisionPublisher(publisher *ucxl.DecisionPublisher) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
t.decisionPublisher = publisher
|
||||
}
|
||||
|
||||
// publishTaskCompletion publishes a task completion decision to DHT
|
||||
func (t *TaskTracker) publishTaskCompletion(taskID string, success bool, summary string, filesModified []string) {
|
||||
if t.decisionPublisher == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.decisionPublisher.PublishTaskCompletion(taskID, success, summary, filesModified); err != nil {
|
||||
fmt.Printf("⚠️ Failed to publish task completion for %s: %v\n", taskID, err)
|
||||
} else {
|
||||
fmt.Printf("📤 Published task completion decision for: %s\n", taskID)
|
||||
}
|
||||
}
|
||||
|
||||
// IsAvailable returns whether the tracker can accept new tasks
|
||||
func (t *TaskTracker) IsAvailable() bool {
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
return len(t.activeTasks) < t.maxTasks
|
||||
}
|
||||
|
||||
// GetStatus returns the current status string
|
||||
func (t *TaskTracker) GetStatus() string {
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
currentTasks := len(t.activeTasks)
|
||||
|
||||
if currentTasks >= t.maxTasks {
|
||||
return "busy"
|
||||
} else if currentTasks > 0 {
|
||||
return "working"
|
||||
}
|
||||
return "ready"
|
||||
}
|
||||
|
||||
// AnnounceAvailability starts a goroutine that broadcasts current working status
|
||||
func (t *TaskTracker) AnnounceAvailability() {
|
||||
if t.pubsub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
t.mutex.RLock()
|
||||
currentTasks := t.GetActiveTasks()
|
||||
maxTasks := t.maxTasks
|
||||
isAvailable := len(currentTasks) < maxTasks
|
||||
status := t.GetStatus()
|
||||
t.mutex.RUnlock()
|
||||
|
||||
availability := map[string]interface{}{
|
||||
"node_id": t.nodeID,
|
||||
"available_for_work": isAvailable,
|
||||
"current_tasks": len(currentTasks),
|
||||
"max_tasks": maxTasks,
|
||||
"last_activity": time.Now().Unix(),
|
||||
"status": status,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := t.pubsub.PublishBzzzMessage(pubsub.AvailabilityBcast, availability); err != nil {
|
||||
fmt.Printf("❌ Failed to announce availability: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CapabilityAnnouncer handles capability announcements
|
||||
type CapabilityAnnouncer struct {
|
||||
pubsub *pubsub.PubSub
|
||||
nodeID string
|
||||
logger interface{} // Using interface to avoid import cycles
|
||||
}
|
||||
|
||||
// NewCapabilityAnnouncer creates a new capability announcer
|
||||
func NewCapabilityAnnouncer(ps *pubsub.PubSub, nodeID string) *CapabilityAnnouncer {
|
||||
return &CapabilityAnnouncer{
|
||||
pubsub: ps,
|
||||
nodeID: nodeID,
|
||||
}
|
||||
}
|
||||
|
||||
// AnnounceCapabilitiesOnChange announces capabilities only when they change
|
||||
func (ca *CapabilityAnnouncer) AnnounceCapabilitiesOnChange(services *RuntimeServices) {
|
||||
if ca.pubsub == nil || services == nil || services.Config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := services.Config
|
||||
|
||||
// Get current capabilities
|
||||
currentCaps := map[string]interface{}{
|
||||
"node_id": ca.nodeID,
|
||||
"capabilities": cfg.Agent.Capabilities,
|
||||
"models": cfg.Agent.Models,
|
||||
"version": "0.2.0",
|
||||
"specialization": cfg.Agent.Specialization,
|
||||
}
|
||||
|
||||
// Load stored capabilities from file
|
||||
storedCaps, err := ca.loadStoredCapabilities(ca.nodeID)
|
||||
if err != nil {
|
||||
fmt.Printf("📄 No stored capabilities found, treating as first run\n")
|
||||
storedCaps = nil
|
||||
}
|
||||
|
||||
// Check if capabilities have changed
|
||||
if ca.capabilitiesChanged(currentCaps, storedCaps) {
|
||||
fmt.Printf("🔄 Capabilities changed, broadcasting update\n")
|
||||
|
||||
currentCaps["timestamp"] = time.Now().Unix()
|
||||
currentCaps["reason"] = ca.getChangeReason(currentCaps, storedCaps)
|
||||
|
||||
// Broadcast the change
|
||||
if err := ca.pubsub.PublishBzzzMessage(pubsub.CapabilityBcast, currentCaps); err != nil {
|
||||
fmt.Printf("❌ Failed to announce capabilities: %v", err)
|
||||
} else {
|
||||
// Store new capabilities
|
||||
if err := ca.storeCapabilities(ca.nodeID, currentCaps); err != nil {
|
||||
fmt.Printf("❌ Failed to store capabilities: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("✅ Capabilities unchanged since last run\n")
|
||||
}
|
||||
}
|
||||
|
||||
// AnnounceRoleOnStartup announces the agent's role when starting up
|
||||
func (ca *CapabilityAnnouncer) AnnounceRoleOnStartup(services *RuntimeServices) {
|
||||
if ca.pubsub == nil || services == nil || services.Config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := services.Config
|
||||
if cfg.Agent.Role == "" {
|
||||
return // No role to announce
|
||||
}
|
||||
|
||||
roleData := map[string]interface{}{
|
||||
"node_id": ca.nodeID,
|
||||
"role": cfg.Agent.Role,
|
||||
"expertise": cfg.Agent.Expertise,
|
||||
"reports_to": cfg.Agent.ReportsTo,
|
||||
"deliverables": cfg.Agent.Deliverables,
|
||||
"capabilities": cfg.Agent.Capabilities,
|
||||
"specialization": cfg.Agent.Specialization,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"status": "online",
|
||||
}
|
||||
|
||||
opts := pubsub.MessageOptions{
|
||||
FromRole: cfg.Agent.Role,
|
||||
RequiredExpertise: cfg.Agent.Expertise,
|
||||
Priority: "medium",
|
||||
}
|
||||
|
||||
if err := ca.pubsub.PublishRoleBasedMessage(pubsub.RoleAnnouncement, roleData, opts); err != nil {
|
||||
fmt.Printf("❌ Failed to announce role: %v", err)
|
||||
} else {
|
||||
fmt.Printf("📢 Role announced: %s\n", cfg.Agent.Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder implementations for capability storage and comparison
|
||||
// These would be implemented similarly to the main.go versions
|
||||
|
||||
func (ca *CapabilityAnnouncer) loadStoredCapabilities(nodeID string) (map[string]interface{}, error) {
|
||||
// Implementation moved from main.go
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (ca *CapabilityAnnouncer) storeCapabilities(nodeID string, capabilities map[string]interface{}) error {
|
||||
// Implementation moved from main.go
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (ca *CapabilityAnnouncer) capabilitiesChanged(current, stored map[string]interface{}) bool {
|
||||
// Implementation moved from main.go
|
||||
return true // Always announce for now
|
||||
}
|
||||
|
||||
func (ca *CapabilityAnnouncer) getChangeReason(current, stored map[string]interface{}) string {
|
||||
// Implementation moved from main.go
|
||||
if stored == nil {
|
||||
return "startup"
|
||||
}
|
||||
return "unknown_change"
|
||||
}
|
||||
|
||||
// StatusReporter provides periodic status updates
|
||||
type StatusReporter struct {
|
||||
node interface{} // P2P node interface
|
||||
logger interface{} // Logger interface
|
||||
}
|
||||
|
||||
// NewStatusReporter creates a new status reporter
|
||||
func NewStatusReporter(node interface{}) *StatusReporter {
|
||||
return &StatusReporter{
|
||||
node: node,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins periodic status reporting
|
||||
func (sr *StatusReporter) Start() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
// This would call the actual node's ConnectedPeers method
|
||||
// peers := sr.node.ConnectedPeers()
|
||||
// fmt.Printf("📊 Status: %d connected peers\n", peers)
|
||||
fmt.Printf("📊 Status: periodic update\n")
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user