Feature: Recurring jobs with professional scheduling dialog

This commit is contained in:
cynfo3000
2026-03-05 09:39:41 +01:00
parent f4a28463b7
commit 721fd5008f
8 changed files with 505 additions and 330 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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")