Feature: Task Scheduler - planned tasks with webhook support
This commit is contained in:
@@ -227,6 +227,24 @@ func (d *Database) migrate() error {
|
||||
d.db.Exec(`INSERT INTO system_settings (key, value) VALUES ($1, '') ON CONFLICT DO NOTHING`, k)
|
||||
}
|
||||
|
||||
// Tasks Tabelle
|
||||
d.db.Exec(`CREATE TABLE IF NOT EXISTS tasks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
||||
action TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'scheduled',
|
||||
scheduled_at TIMESTAMPTZ NOT NULL,
|
||||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
result JSONB,
|
||||
webhook_url TEXT,
|
||||
webhook_sent BOOLEAN DEFAULT FALSE,
|
||||
webhook_response TEXT,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`)
|
||||
d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_status_scheduled ON tasks(status, scheduled_at)`)
|
||||
|
||||
// Retention Policy (90 Tage)
|
||||
d.setupRetention()
|
||||
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user