diff --git a/backend/api/scheduler.go b/backend/api/scheduler.go index 82a1b0a..fea1e59 100644 --- a/backend/api/scheduler.go +++ b/backend/api/scheduler.go @@ -62,22 +62,40 @@ func (s *TaskScheduler) executeTask(task models.Task) { case "backup": result, taskErr = s.executeBackup(task.AgentID) case "update": - result, taskErr = s.executeUpdate(task.AgentID, task.Reboot) + result, taskErr = s.executeUpdateWithHealthCheck(task.AgentID, task.Reboot, task.HealthCheck, task.HealthCheckTimeout) default: taskErr = fmt.Errorf("unbekannte Aktion: %s", task.Action) } success := taskErr == nil + healthCheckWarning := false if !success { - result = map[string]string{"error": taskErr.Error()} + // Check if it's a health check warning (update ran OK but agent didn't come back) + if resultMap, ok := result.(map[string]interface{}); ok { + if hc, ok := resultMap["health_check"].(map[string]interface{}); ok { + if online, ok := hc["agent_online"].(bool); ok && !online { + healthCheckWarning = true + } + } + } + if !healthCheckWarning { + result = map[string]string{"error": taskErr.Error()} + } log.Printf("Scheduler: Task #%d fehlgeschlagen: %v", task.ID, taskErr) } else { log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", task.ID) } // Use CompleteRecurringTask for proper recurring handling + // Health check warnings count as success for recurring schedule, but status = "completed_with_warning" + scheduleSuccess := success || healthCheckWarning s.db.CompleteRecurringTask(task.ID, task.ScheduleType, task.ScheduleTime, - task.ScheduleWeekday, task.ScheduleMonthday, success, result) + task.ScheduleWeekday, task.ScheduleMonthday, scheduleSuccess, result) + + // Override status for health check warnings + if healthCheckWarning { + s.db.UpdateTaskStatusDirect(task.ID, "completed_with_warning") + } // Re-read task for webhook payload fullTask, _ := s.db.GetTask(task.ID) @@ -156,6 +174,10 @@ func (s *TaskScheduler) executeBackup(agentID string) (interface{}, error) { } func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{}, error) { + return s.executeUpdateWithHealthCheck(agentID, reboot, false, 600) +} + +func (s *TaskScheduler) executeUpdateWithHealthCheck(agentID string, reboot bool, healthCheck bool, healthCheckTimeout int) (interface{}, error) { if !s.hub.IsAgentConnected(agentID) { return nil, fmt.Errorf("Agent nicht verbunden") } @@ -201,6 +223,7 @@ func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{}, return nil, fmt.Errorf("Update-Command senden fehlgeschlagen: %w", err) } + var updateResult map[string]interface{} select { case resp := <-updateCh: respStatus, _ := resp["status"].(string) @@ -208,15 +231,48 @@ func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{}, errMsg, _ := resp["error"].(string) return resp, fmt.Errorf("Update fehlgeschlagen: %s", errMsg) } - return map[string]interface{}{ - "message": "Update ausgefuehrt", - "check_result": checkResult, - "update_result": resp, - "reboot": reboot, - }, nil + updateResult = resp case <-time.After(900 * time.Second): return nil, fmt.Errorf("Timeout beim Update") } + + result := map[string]interface{}{ + "message": "Update ausgefuehrt", + "check_result": checkResult, + "update_result": updateResult, + "reboot": reboot, + } + + // Health Check: wait for agent to come back online + if healthCheck { + timeout := time.Duration(healthCheckTimeout) * time.Second + if timeout <= 0 { + timeout = 600 * time.Second + } + log.Printf("Scheduler: Health Check fuer Agent %s (Timeout: %v)", agentID, timeout) + + // If no reboot, agent should still be connected — give it a moment for post-update restart + if !reboot { + time.Sleep(15 * time.Second) + } + + checkedAt := time.Now().UTC().Format(time.RFC3339) + agentOnline := s.hub.WaitForAgentOnline(agentID, timeout) + result["health_check"] = map[string]interface{}{ + "agent_online": agentOnline, + "checked_at": checkedAt, + } + + if !agentOnline { + result["message"] = "Update ausgefuehrt, aber Agent nicht wieder online (Health Check fehlgeschlagen)" + log.Printf("Scheduler: Health Check fehlgeschlagen — Agent %s nach %v nicht online", agentID, timeout) + // Return result but signal warning via error + return result, fmt.Errorf("Health Check: Agent nach %v nicht online", timeout) + } + log.Printf("Scheduler: Health Check OK — Agent %s wieder online", agentID) + } + + return result, nil } func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) { diff --git a/backend/db/tasks.go b/backend/db/tasks.go index 4abb1dc..4bb156f 100644 --- a/backend/db/tasks.go +++ b/backend/db/tasks.go @@ -126,14 +126,14 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) { var startedAt, finishedAt, lastRun, nextRun sql.NullTime var resultJSON sql.NullString var webhookURL, webhookResponse, createdBy sql.NullString - var agentName, customerName sql.NullString + var agentName, customerName, customerNumber sql.NullString var name, scheduleType, scheduleTime sql.NullString var scheduleWeekday, scheduleMonthday sql.NullInt32 var reboot, healthCheck, active sql.NullBool var healthCheckTimeout, runCount sql.NullInt32 err := scan( - &t.ID, &t.AgentID, &agentName, &customerName, + &t.ID, &t.AgentID, &agentName, &customerName, &customerNumber, &name, &t.Action, &t.Status, &scheduledAt, &startedAt, &finishedAt, &resultJSON, &webhookURL, &t.WebhookSent, &webhookResponse, &createdBy, &createdAt, @@ -146,6 +146,7 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) { t.AgentName = agentName.String t.CustomerName = customerName.String + t.CustomerNumber = customerNumber.String t.Name = name.String t.ScheduleType = scheduleType.String if t.ScheduleType == "" { @@ -208,7 +209,7 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) { return &t, nil } -const taskSelectCols = `t.id, t.agent_id, COALESCE(a.name, ''), COALESCE(c.name, ''), +const taskSelectCols = `t.id, t.agent_id, COALESCE(a.name, ''), COALESCE(c.name, ''), COALESCE(c.number, ''), t.name, t.action, t.status, t.scheduled_at, t.started_at, t.finished_at, t.result, t.webhook_url, t.webhook_sent, t.webhook_response, t.created_by, t.created_at, @@ -415,6 +416,12 @@ func (d *Database) CancelTask(id int) error { return nil } +// UpdateTaskStatusDirect sets only the status field (for post-completion overrides). +func (d *Database) UpdateTaskStatusDirect(id int, status string) error { + _, err := d.db.Exec("UPDATE tasks SET status = $1 WHERE id = $2", status, id) + return err +} + func (d *Database) DeleteTask(id int) error { res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id) if err != nil { diff --git a/backend/models/task.go b/backend/models/task.go index d26a549..5d98151 100644 --- a/backend/models/task.go +++ b/backend/models/task.go @@ -5,6 +5,7 @@ type Task struct { AgentID string `json:"agent_id"` AgentName string `json:"agent_name,omitempty"` CustomerName string `json:"customer_name,omitempty"` + CustomerNumber string `json:"customer_number,omitempty"` Name string `json:"name"` Action string `json:"action"` Status string `json:"status"` diff --git a/backend/ws/hub.go b/backend/ws/hub.go index 7cfea7f..4d9e1a8 100644 --- a/backend/ws/hub.go +++ b/backend/ws/hub.go @@ -175,6 +175,19 @@ func (h *Hub) IsAgentConnected(agentID string) bool { return exists } +// WaitForAgentOnline polls until the agent reconnects or timeout is reached. +// Returns true if agent came online within the timeout, false otherwise. +func (h *Hub) WaitForAgentOnline(agentID string, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if h.IsAgentConnected(agentID) { + return true + } + time.Sleep(10 * time.Second) + } + return false +} + func (h *Hub) GetConnectedAgents() []string { h.agentsMux.RLock() defer h.agentsMux.RUnlock() diff --git a/frontend/src/components/AgentPanel.jsx b/frontend/src/components/AgentPanel.jsx index ddd4f74..791940c 100644 --- a/frontend/src/components/AgentPanel.jsx +++ b/frontend/src/components/AgentPanel.jsx @@ -1821,7 +1821,13 @@ function TasksTab({ agentId, agentName }) { {tasks.map(t => ( - +
+ + {t.status === 'running' && lauft} + {t.status === 'failed' && fehler} + {t.status === 'completed_with_warning' && warnung} + {t.status === 'cancelled' && abgebr.} +
{t.name || `Job #${t.id}`} {t.action}