feat: Task Health Check + customer_number in webhook payload
- Task model: CustomerNumber field (K-Nummer aus customers.number)
- DB: taskSelectCols joined c.number, scanTask scans CustomerNumber
- Hub: WaitForAgentOnline() polls until agent reconnects or timeout
- Scheduler: executeUpdateWithHealthCheck() waits for agent reconnect after update
- health_check: {agent_online: bool, checked_at: RFC3339} in result
- Status: completed_with_warning wenn Update OK aber Agent offline bleibt
- Frontend: Task-Tabelle zeigt Status-Badge (lauft/fehler/warnung/abgebr.)
This commit is contained in:
parent
721fd5008f
commit
d4b6ca86e5
@ -62,22 +62,40 @@ func (s *TaskScheduler) executeTask(task models.Task) {
|
|||||||
case "backup":
|
case "backup":
|
||||||
result, taskErr = s.executeBackup(task.AgentID)
|
result, taskErr = s.executeBackup(task.AgentID)
|
||||||
case "update":
|
case "update":
|
||||||
result, taskErr = s.executeUpdate(task.AgentID, task.Reboot)
|
result, taskErr = s.executeUpdateWithHealthCheck(task.AgentID, task.Reboot, task.HealthCheck, task.HealthCheckTimeout)
|
||||||
default:
|
default:
|
||||||
taskErr = fmt.Errorf("unbekannte Aktion: %s", task.Action)
|
taskErr = fmt.Errorf("unbekannte Aktion: %s", task.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
success := taskErr == nil
|
success := taskErr == nil
|
||||||
|
healthCheckWarning := false
|
||||||
if !success {
|
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)
|
log.Printf("Scheduler: Task #%d fehlgeschlagen: %v", task.ID, taskErr)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", task.ID)
|
log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", task.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use CompleteRecurringTask for proper recurring handling
|
// 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,
|
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
|
// Re-read task for webhook payload
|
||||||
fullTask, _ := s.db.GetTask(task.ID)
|
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) {
|
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) {
|
if !s.hub.IsAgentConnected(agentID) {
|
||||||
return nil, fmt.Errorf("Agent nicht verbunden")
|
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)
|
return nil, fmt.Errorf("Update-Command senden fehlgeschlagen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updateResult map[string]interface{}
|
||||||
select {
|
select {
|
||||||
case resp := <-updateCh:
|
case resp := <-updateCh:
|
||||||
respStatus, _ := resp["status"].(string)
|
respStatus, _ := resp["status"].(string)
|
||||||
@ -208,15 +231,48 @@ func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{},
|
|||||||
errMsg, _ := resp["error"].(string)
|
errMsg, _ := resp["error"].(string)
|
||||||
return resp, fmt.Errorf("Update fehlgeschlagen: %s", errMsg)
|
return resp, fmt.Errorf("Update fehlgeschlagen: %s", errMsg)
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
updateResult = resp
|
||||||
"message": "Update ausgefuehrt",
|
|
||||||
"check_result": checkResult,
|
|
||||||
"update_result": resp,
|
|
||||||
"reboot": reboot,
|
|
||||||
}, nil
|
|
||||||
case <-time.After(900 * time.Second):
|
case <-time.After(900 * time.Second):
|
||||||
return nil, fmt.Errorf("Timeout beim Update")
|
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{}) {
|
func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) {
|
||||||
|
|||||||
@ -126,14 +126,14 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) {
|
|||||||
var startedAt, finishedAt, lastRun, nextRun sql.NullTime
|
var startedAt, finishedAt, lastRun, nextRun sql.NullTime
|
||||||
var resultJSON sql.NullString
|
var resultJSON sql.NullString
|
||||||
var webhookURL, webhookResponse, createdBy 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 name, scheduleType, scheduleTime sql.NullString
|
||||||
var scheduleWeekday, scheduleMonthday sql.NullInt32
|
var scheduleWeekday, scheduleMonthday sql.NullInt32
|
||||||
var reboot, healthCheck, active sql.NullBool
|
var reboot, healthCheck, active sql.NullBool
|
||||||
var healthCheckTimeout, runCount sql.NullInt32
|
var healthCheckTimeout, runCount sql.NullInt32
|
||||||
|
|
||||||
err := scan(
|
err := scan(
|
||||||
&t.ID, &t.AgentID, &agentName, &customerName,
|
&t.ID, &t.AgentID, &agentName, &customerName, &customerNumber,
|
||||||
&name, &t.Action, &t.Status, &scheduledAt, &startedAt, &finishedAt,
|
&name, &t.Action, &t.Status, &scheduledAt, &startedAt, &finishedAt,
|
||||||
&resultJSON, &webhookURL, &t.WebhookSent, &webhookResponse,
|
&resultJSON, &webhookURL, &t.WebhookSent, &webhookResponse,
|
||||||
&createdBy, &createdAt,
|
&createdBy, &createdAt,
|
||||||
@ -146,6 +146,7 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) {
|
|||||||
|
|
||||||
t.AgentName = agentName.String
|
t.AgentName = agentName.String
|
||||||
t.CustomerName = customerName.String
|
t.CustomerName = customerName.String
|
||||||
|
t.CustomerNumber = customerNumber.String
|
||||||
t.Name = name.String
|
t.Name = name.String
|
||||||
t.ScheduleType = scheduleType.String
|
t.ScheduleType = scheduleType.String
|
||||||
if t.ScheduleType == "" {
|
if t.ScheduleType == "" {
|
||||||
@ -208,7 +209,7 @@ func scanTask(scan func(dest ...interface{}) error) (*models.Task, error) {
|
|||||||
return &t, nil
|
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.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.result, t.webhook_url, t.webhook_sent, t.webhook_response,
|
||||||
t.created_by, t.created_at,
|
t.created_by, t.created_at,
|
||||||
@ -415,6 +416,12 @@ func (d *Database) CancelTask(id int) error {
|
|||||||
return nil
|
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 {
|
func (d *Database) DeleteTask(id int) error {
|
||||||
res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id)
|
res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ type Task struct {
|
|||||||
AgentID string `json:"agent_id"`
|
AgentID string `json:"agent_id"`
|
||||||
AgentName string `json:"agent_name,omitempty"`
|
AgentName string `json:"agent_name,omitempty"`
|
||||||
CustomerName string `json:"customer_name,omitempty"`
|
CustomerName string `json:"customer_name,omitempty"`
|
||||||
|
CustomerNumber string `json:"customer_number,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|||||||
@ -175,6 +175,19 @@ func (h *Hub) IsAgentConnected(agentID string) bool {
|
|||||||
return exists
|
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 {
|
func (h *Hub) GetConnectedAgents() []string {
|
||||||
h.agentsMux.RLock()
|
h.agentsMux.RLock()
|
||||||
defer h.agentsMux.RUnlock()
|
defer h.agentsMux.RUnlock()
|
||||||
|
|||||||
@ -1821,7 +1821,13 @@ function TasksTab({ agentId, agentName }) {
|
|||||||
{tasks.map(t => (
|
{tasks.map(t => (
|
||||||
<tr key={t.id} className={!t.active ? 'opacity-50' : ''}>
|
<tr key={t.id} className={!t.active ? 'opacity-50' : ''}>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<span className={`inline-block w-2.5 h-2.5 rounded-full ${t.active ? 'bg-green-500' : 'bg-gray-500'}`} title={t.active ? 'Aktiv' : 'Inaktiv'} />
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className={`inline-block w-2 h-2 rounded-full flex-shrink-0 ${t.active ? 'bg-green-500' : 'bg-gray-500'}`} title={t.active ? 'Aktiv' : 'Inaktiv'} />
|
||||||
|
{t.status === 'running' && <span className="text-xs px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-300">lauft</span>}
|
||||||
|
{t.status === 'failed' && <span className="text-xs px-1.5 py-0.5 rounded bg-red-900/50 text-red-300">fehler</span>}
|
||||||
|
{t.status === 'completed_with_warning' && <span className="text-xs px-1.5 py-0.5 rounded bg-yellow-900/50 text-yellow-300" title="Update OK, aber Agent nicht wieder online">warnung</span>}
|
||||||
|
{t.status === 'cancelled' && <span className="text-xs px-1.5 py-0.5 rounded bg-gray-700 text-gray-400">abgebr.</span>}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-white">{t.name || `Job #${t.id}`}</td>
|
<td className="px-3 py-2 text-white">{t.name || `Job #${t.id}`}</td>
|
||||||
<td className="px-3 py-2 text-gray-300 capitalize">{t.action}</td>
|
<td className="px-3 py-2 text-gray-300 capitalize">{t.action}</td>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user