Feature: Recurring jobs with professional scheduling dialog
This commit is contained in:
@@ -86,6 +86,7 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
||||
mux.HandleFunc("POST /api/v1/tasks", h.createTask(hub))
|
||||
mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.deleteTask)
|
||||
mux.HandleFunc("POST /api/v1/tasks/{id}/cancel", h.cancelTask)
|
||||
mux.HandleFunc("PUT /api/v1/tasks/{id}/toggle", h.toggleTask)
|
||||
|
||||
// WebSocket und Tunnel-Routes
|
||||
h.setupTunnelRoutes(mux, hub)
|
||||
|
||||
+24
-23
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cynfo/rmm-backend/db"
|
||||
"github.com/cynfo/rmm-backend/models"
|
||||
"github.com/cynfo/rmm-backend/ws"
|
||||
)
|
||||
|
||||
@@ -44,46 +45,46 @@ func (s *TaskScheduler) processDueTasks() {
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
go s.executeTask(task.ID, task.AgentID, task.Action, task.WebhookURL, task.AgentName, task.CustomerName, task.ScheduledAt)
|
||||
go s.executeTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskScheduler) executeTask(taskID int, agentID, action, webhookURL, agentName, customerName, scheduledAt string) {
|
||||
log.Printf("Scheduler: Starte Task #%d (%s) fuer Agent %s", taskID, action, agentID)
|
||||
func (s *TaskScheduler) executeTask(task models.Task) {
|
||||
log.Printf("Scheduler: Starte Task #%d (%s) fuer Agent %s", task.ID, task.Action, task.AgentID)
|
||||
|
||||
// Set status to running
|
||||
s.db.UpdateTaskStatus(taskID, "running", nil)
|
||||
s.db.UpdateTaskStatus(task.ID, "running", nil)
|
||||
|
||||
var result interface{}
|
||||
var taskErr error
|
||||
|
||||
switch action {
|
||||
switch task.Action {
|
||||
case "backup":
|
||||
result, taskErr = s.executeBackup(agentID)
|
||||
result, taskErr = s.executeBackup(task.AgentID)
|
||||
case "update":
|
||||
result, taskErr = s.executeUpdate(agentID)
|
||||
result, taskErr = s.executeUpdate(task.AgentID, task.Reboot)
|
||||
default:
|
||||
taskErr = fmt.Errorf("unbekannte Aktion: %s", action)
|
||||
taskErr = fmt.Errorf("unbekannte Aktion: %s", task.Action)
|
||||
}
|
||||
|
||||
// Update status
|
||||
status := "completed"
|
||||
if taskErr != nil {
|
||||
status = "failed"
|
||||
success := taskErr == nil
|
||||
if !success {
|
||||
result = map[string]string{"error": taskErr.Error()}
|
||||
log.Printf("Scheduler: Task #%d fehlgeschlagen: %v", taskID, taskErr)
|
||||
log.Printf("Scheduler: Task #%d fehlgeschlagen: %v", task.ID, taskErr)
|
||||
} else {
|
||||
log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", taskID)
|
||||
log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", task.ID)
|
||||
}
|
||||
|
||||
s.db.UpdateTaskStatus(taskID, status, result)
|
||||
// Use CompleteRecurringTask for proper recurring handling
|
||||
s.db.CompleteRecurringTask(task.ID, task.ScheduleType, task.ScheduleTime,
|
||||
task.ScheduleWeekday, task.ScheduleMonthday, success, result)
|
||||
|
||||
// Re-read task for webhook payload
|
||||
task, _ := s.db.GetTask(taskID)
|
||||
fullTask, _ := s.db.GetTask(task.ID)
|
||||
|
||||
// Send webhook if configured
|
||||
if webhookURL != "" && task != nil {
|
||||
s.sendWebhook(taskID, webhookURL, task)
|
||||
if task.WebhookURL != "" && fullTask != nil {
|
||||
s.sendWebhook(task.ID, task.WebhookURL, fullTask)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +122,6 @@ func (s *TaskScheduler) executeBackup(agentID string) (interface{}, error) {
|
||||
return resp, fmt.Errorf("ungueltiges Response-Format")
|
||||
}
|
||||
|
||||
// Save backup to DB (same logic as triggerBackup handler)
|
||||
configB64, _ := data["config"].(string)
|
||||
hash, _ := data["hash"].(string)
|
||||
sizeFloat, _ := data["size"].(float64)
|
||||
@@ -155,7 +155,7 @@ func (s *TaskScheduler) executeBackup(agentID string) (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskScheduler) executeUpdate(agentID string) (interface{}, error) {
|
||||
func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{}, error) {
|
||||
if !s.hub.IsAgentConnected(agentID) {
|
||||
return nil, fmt.Errorf("Agent nicht verbunden")
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func (s *TaskScheduler) executeUpdate(agentID string) (interface{}, error) {
|
||||
"type": "command",
|
||||
"id": updateCmdID,
|
||||
"command": "update",
|
||||
"params": map[string]interface{}{"reboot": false},
|
||||
"params": map[string]interface{}{"reboot": reboot},
|
||||
}
|
||||
|
||||
updateCh := s.hub.RegisterResponseWaiter(updateCmdID)
|
||||
@@ -209,9 +209,10 @@ func (s *TaskScheduler) executeUpdate(agentID string) (interface{}, error) {
|
||||
return resp, fmt.Errorf("Update fehlgeschlagen: %s", errMsg)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"message": "Update ausgefuehrt",
|
||||
"check_result": checkResult,
|
||||
"message": "Update ausgefuehrt",
|
||||
"check_result": checkResult,
|
||||
"update_result": resp,
|
||||
"reboot": reboot,
|
||||
}, nil
|
||||
case <-time.After(900 * time.Second):
|
||||
return nil, fmt.Errorf("Timeout beim Update")
|
||||
|
||||
+52
-15
@@ -68,10 +68,19 @@ func (h *Handler) getTask(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) createTask(hub *ws.Hub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Action string `json:"action"`
|
||||
ScheduledAt string `json:"scheduled_at"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Name string `json:"name"`
|
||||
Action string `json:"action"`
|
||||
ScheduledAt string `json:"scheduled_at"`
|
||||
ScheduleType string `json:"schedule_type"`
|
||||
ScheduleTime string `json:"schedule_time"`
|
||||
ScheduleWeekday *int `json:"schedule_weekday"`
|
||||
ScheduleMonthday *int `json:"schedule_monthday"`
|
||||
Reboot bool `json:"reboot"`
|
||||
HealthCheck bool `json:"health_check"`
|
||||
HealthCheckTimeout int `json:"health_check_timeout"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
Active *bool `json:"active"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
|
||||
@@ -83,26 +92,33 @@ func (h *Handler) createTask(hub *ws.Hub) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate action
|
||||
if req.Action != "backup" && req.Action != "update" {
|
||||
writeError(w, http.StatusBadRequest, "Ungueltige Aktion (backup, update)")
|
||||
return
|
||||
}
|
||||
|
||||
// Check agent exists
|
||||
agent, _, err := h.db.GetAgent(req.AgentID)
|
||||
if err != nil || agent == nil {
|
||||
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from context
|
||||
createdBy := "api-key"
|
||||
if claims, ok := r.Context().Value(UserContextKey).(*JWTClaims); ok {
|
||||
createdBy = claims.Username
|
||||
}
|
||||
|
||||
task, err := h.db.CreateTask(req.AgentID, req.Action, req.ScheduledAt, req.WebhookURL, createdBy)
|
||||
active := true
|
||||
if req.Active != nil {
|
||||
active = *req.Active
|
||||
}
|
||||
|
||||
task, err := h.db.CreateTask(
|
||||
req.AgentID, req.Action, req.ScheduledAt, req.WebhookURL, createdBy,
|
||||
req.Name, req.ScheduleType, req.ScheduleTime,
|
||||
req.ScheduleWeekday, req.ScheduleMonthday,
|
||||
req.Reboot, req.HealthCheck, req.HealthCheckTimeout, active,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Task erstellen fehlgeschlagen: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "Task erstellen fehlgeschlagen: "+err.Error())
|
||||
@@ -111,13 +127,11 @@ func (h *Handler) createTask(hub *ws.Hub) http.HandlerFunc {
|
||||
|
||||
h.auditLog(r, "task.create", "task", strconv.Itoa(task.ID), agent.Name, req.Action)
|
||||
|
||||
// If scheduled_at is in the past or now, trigger immediate execution
|
||||
if req.ScheduledAt == "" {
|
||||
// Already scheduled for now, scheduler will pick it up within 30s
|
||||
} else {
|
||||
schedTime, err := time.Parse(time.RFC3339, req.ScheduledAt)
|
||||
if err == nil && !schedTime.After(time.Now()) {
|
||||
// In the past — scheduler picks it up
|
||||
// If once and scheduled_at is in the past or now, scheduler picks it up within 30s
|
||||
if req.ScheduledAt != "" && req.ScheduleType == "once" {
|
||||
if schedTime, err := time.Parse(time.RFC3339, req.ScheduledAt); err == nil && !schedTime.After(time.Now()) {
|
||||
// scheduler will pick it up
|
||||
_ = schedTime
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +139,29 @@ func (h *Handler) createTask(hub *ws.Hub) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/v1/tasks/{id}/toggle
|
||||
func (h *Handler) toggleTask(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
||||
return
|
||||
}
|
||||
|
||||
task, err := h.db.ToggleTask(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Umschalten")
|
||||
return
|
||||
}
|
||||
if task == nil {
|
||||
writeError(w, http.StatusNotFound, "Task nicht gefunden")
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(r, "task.toggle", "task", idStr, "", "")
|
||||
writeJSON(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
// POST /api/v1/tasks/{id}/cancel
|
||||
func (h *Handler) cancelTask(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
|
||||
Reference in New Issue
Block a user