package db import ( "database/sql" "encoding/json" "fmt" "strings" "time" "github.com/cynfo/rmm-backend/models" ) func (d *Database) CreateTask(agentID, action, scheduledAt, webhookURL, createdBy string) (*models.Task, error) { // Validate action validActions := map[string]bool{"backup": true, "update": true} if !validActions[action] { return nil, fmt.Errorf("ungueltige Aktion: %s", action) } var schedTime time.Time var err error if scheduledAt == "" { schedTime = time.Now() } else { schedTime, err = time.Parse(time.RFC3339, scheduledAt) if err != nil { return nil, fmt.Errorf("ungueltiges Datum: %w", err) } } var task models.Task err = d.db.QueryRow(` INSERT INTO tasks (agent_id, action, scheduled_at, webhook_url, created_by) VALUES ($1, $2, $3, $4, $5) RETURNING id, agent_id, action, status, scheduled_at, webhook_url, COALESCE(created_by, ''), created_at `, agentID, action, schedTime, webhookURL, createdBy).Scan( &task.ID, &task.AgentID, &task.Action, &task.Status, &schedTime, &task.WebhookURL, &task.CreatedBy, &schedTime, ) if err != nil { return nil, fmt.Errorf("Task erstellen: %w", err) } // Re-read cleanly return d.GetTask(task.ID) } func (d *Database) GetTask(id int) (*models.Task, error) { var t models.Task var scheduledAt, createdAt time.Time var startedAt, finishedAt sql.NullTime var resultJSON sql.NullString var webhookURL, webhookResponse, createdBy sql.NullString var agentName, customerName sql.NullString err := d.db.QueryRow(` SELECT t.id, t.agent_id, COALESCE(a.name, ''), COALESCE(c.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 FROM tasks t LEFT JOIN agents a ON a.id = t.agent_id LEFT JOIN customers c ON c.id = a.customer_id WHERE t.id = $1 `, id).Scan( &t.ID, &t.AgentID, &agentName, &customerName, &t.Action, &t.Status, &scheduledAt, &startedAt, &finishedAt, &resultJSON, &webhookURL, &t.WebhookSent, &webhookResponse, &createdBy, &createdAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } t.AgentName = agentName.String t.CustomerName = customerName.String t.ScheduledAt = scheduledAt.Format(time.RFC3339) t.CreatedAt = createdAt.Format(time.RFC3339) if startedAt.Valid { s := startedAt.Time.Format(time.RFC3339) t.StartedAt = &s } if finishedAt.Valid { s := finishedAt.Time.Format(time.RFC3339) t.FinishedAt = &s } if resultJSON.Valid { var r interface{} json.Unmarshal([]byte(resultJSON.String), &r) t.Result = r } if webhookURL.Valid { t.WebhookURL = webhookURL.String } if webhookResponse.Valid { t.WebhookResponse = webhookResponse.String } if createdBy.Valid { t.CreatedBy = createdBy.String } return &t, nil } func (d *Database) GetTasks(limit, offset int, agentID, status, action string) ([]models.Task, int, error) { if limit <= 0 { limit = 50 } where := []string{} args := []interface{}{} argIdx := 1 if agentID != "" { where = append(where, fmt.Sprintf("t.agent_id = $%d", argIdx)) args = append(args, agentID) argIdx++ } if status != "" { where = append(where, fmt.Sprintf("t.status = $%d", argIdx)) args = append(args, status) argIdx++ } if action != "" { where = append(where, fmt.Sprintf("t.action = $%d", argIdx)) args = append(args, action) argIdx++ } whereClause := "" if len(where) > 0 { whereClause = "WHERE " + strings.Join(where, " AND ") } // Count var total int countQuery := "SELECT COUNT(*) FROM tasks t " + whereClause if err := d.db.QueryRow(countQuery, args...).Scan(&total); err != nil { return nil, 0, err } // Data query := fmt.Sprintf(` SELECT t.id, t.agent_id, COALESCE(a.name, ''), COALESCE(c.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 FROM tasks t LEFT JOIN agents a ON a.id = t.agent_id LEFT JOIN customers c ON c.id = a.customer_id %s ORDER BY t.created_at DESC LIMIT $%d OFFSET $%d `, whereClause, argIdx, argIdx+1) args = append(args, limit, offset) rows, err := d.db.Query(query, args...) if err != nil { return nil, 0, err } defer rows.Close() var tasks []models.Task for rows.Next() { var t models.Task var scheduledAt, createdAt time.Time var startedAt, finishedAt sql.NullTime var resultJSON sql.NullString var webhookURL, webhookResponse, createdBy sql.NullString var agentName, customerName sql.NullString if err := rows.Scan( &t.ID, &t.AgentID, &agentName, &customerName, &t.Action, &t.Status, &scheduledAt, &startedAt, &finishedAt, &resultJSON, &webhookURL, &t.WebhookSent, &webhookResponse, &createdBy, &createdAt, ); err != nil { return nil, 0, err } t.AgentName = agentName.String t.CustomerName = customerName.String t.ScheduledAt = scheduledAt.Format(time.RFC3339) t.CreatedAt = createdAt.Format(time.RFC3339) if startedAt.Valid { s := startedAt.Time.Format(time.RFC3339) t.StartedAt = &s } if finishedAt.Valid { s := finishedAt.Time.Format(time.RFC3339) t.FinishedAt = &s } if resultJSON.Valid { var r interface{} json.Unmarshal([]byte(resultJSON.String), &r) t.Result = r } if webhookURL.Valid { t.WebhookURL = webhookURL.String } if webhookResponse.Valid { t.WebhookResponse = webhookResponse.String } if createdBy.Valid { t.CreatedBy = createdBy.String } tasks = append(tasks, t) } if tasks == nil { tasks = []models.Task{} } return tasks, total, rows.Err() } func (d *Database) UpdateTaskStatus(id int, status string, result interface{}) error { var resultJSON *string if result != nil { b, err := json.Marshal(result) if err == nil { s := string(b) resultJSON = &s } } now := time.Now().UTC() switch status { case "running": _, err := d.db.Exec( "UPDATE tasks SET status = $1, started_at = $2 WHERE id = $3", status, now, id, ) return err case "completed", "failed": _, err := d.db.Exec( "UPDATE tasks SET status = $1, finished_at = $2, result = $3 WHERE id = $4", status, now, resultJSON, id, ) return err default: _, err := d.db.Exec("UPDATE tasks SET status = $1 WHERE id = $2", status, id) return err } } func (d *Database) SetTaskWebhookResult(id int, sent bool, response string) error { // Truncate response to 500 chars if len(response) > 500 { response = response[:500] } _, err := d.db.Exec( "UPDATE tasks SET webhook_sent = $1, webhook_response = $2 WHERE id = $3", sent, response, id, ) return err } func (d *Database) GetDueTasks() ([]models.Task, error) { rows, err := d.db.Query(` SELECT t.id, t.agent_id, COALESCE(a.name, ''), COALESCE(c.name, ''), t.action, t.status, t.scheduled_at, t.webhook_url, t.created_at FROM tasks t LEFT JOIN agents a ON a.id = t.agent_id LEFT JOIN customers c ON c.id = a.customer_id WHERE t.status = 'scheduled' AND t.scheduled_at <= NOW() ORDER BY t.scheduled_at ASC `) if err != nil { return nil, err } defer rows.Close() var tasks []models.Task for rows.Next() { var t models.Task var scheduledAt, createdAt time.Time var webhookURL sql.NullString var agentName, customerName sql.NullString if err := rows.Scan( &t.ID, &t.AgentID, &agentName, &customerName, &t.Action, &t.Status, &scheduledAt, &webhookURL, &createdAt, ); err != nil { return nil, err } t.AgentName = agentName.String t.CustomerName = customerName.String t.ScheduledAt = scheduledAt.Format(time.RFC3339) t.CreatedAt = createdAt.Format(time.RFC3339) if webhookURL.Valid { t.WebhookURL = webhookURL.String } tasks = append(tasks, t) } return tasks, rows.Err() } func (d *Database) CancelTask(id int) error { res, err := d.db.Exec( "UPDATE tasks SET status = 'cancelled' WHERE id = $1 AND status = 'scheduled'", id, ) if err != nil { return err } rows, _ := res.RowsAffected() if rows == 0 { return fmt.Errorf("Task nicht gefunden oder nicht mehr im Status 'scheduled'") } return nil } func (d *Database) DeleteTask(id int) error { res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id) if err != nil { return err } rows, _ := res.RowsAffected() if rows == 0 { return fmt.Errorf("Task nicht gefunden") } return nil }