package httpapi import ( "context" "encoding/json" "net/http" "time" "github.com/vasyansk/imap-copier/internal/scheduler" "github.com/vasyansk/imap-copier/internal/store" ) func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { var t store.Task if err := json.NewDecoder(r.Body).Decode(&t); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } id, err := s.store.CreateTask(r.Context(), t) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusCreated, map[string]int64{"id": id}) } func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) { tasks, err := s.store.ListTasks(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, tasks) } func (s *Server) handleGetTask(w http.ResponseWriter, r *http.Request) { id, err := pathID(r, "id") if err != nil { http.Error(w, "bad id", http.StatusBadRequest) return } task, err := s.store.GetTask(r.Context(), id) if err != nil { http.Error(w, "not found", http.StatusNotFound) return } accs, err := s.store.ListAccountsByTask(r.Context(), id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } views := make([]AccountView, 0, len(accs)) for _, a := range accs { views = append(views, accountDTO(a)) } writeJSON(w, http.StatusOK, map[string]any{"task": s.taskViewFor(r.Context(), task), "accounts": views}) } // taskView augments the stored task with the server-computed next scheduled run // (RFC3339 UTC; null when the schedule is off, the task is running, or broken). type taskView struct { store.Task NextRunAt *string `json:"next_run_at"` } func (s *Server) taskViewFor(ctx context.Context, t store.Task) taskView { var nextRunAt *string if t.ScheduleIntervalSeconds > 0 && !t.Broken && t.Status != "running" && t.ScheduleAnchor != nil { last, _ := s.store.LastFinishedRunAt(ctx, t.ID) n := scheduler.NextRun(time.Duration(t.ScheduleIntervalSeconds)*time.Second, *t.ScheduleAnchor, last) str := n.UTC().Format(time.RFC3339) nextRunAt = &str } return taskView{Task: t, NextRunAt: nextRunAt} } // handleSetSchedule enables/changes/disables a task's recurring schedule. // Enabling (interval>0) requires every account to pass its connection tests. func (s *Server) handleSetSchedule(w http.ResponseWriter, r *http.Request) { taskID, err := pathID(r, "id") if err != nil { http.Error(w, "bad id", http.StatusBadRequest) return } var body struct { IntervalSeconds int64 `json:"interval_seconds"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if body.IntervalSeconds > 0 { accs, err := s.store.ListAccountsByTask(r.Context(), taskID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if !allAccountsTested(accs) { http.Error(w, "all accounts must pass connection tests before scheduling", http.StatusConflict) return } } if err := s.store.SetTaskSchedule(r.Context(), taskID, body.IntervalSeconds); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // allAccountsTested mirrors the orchestrator's run gate: non-empty and every // account OK on both sides. func allAccountsTested(accs []store.Account) bool { if len(accs) == 0 { return false } for _, a := range accs { if a.TestSrcStatus != "ok" || a.TestDstStatus != "ok" { return false } } return true } func (s *Server) handleListRuns(w http.ResponseWriter, r *http.Request) { taskID, err := pathID(r, "id") if err != nil { http.Error(w, "bad id", http.StatusBadRequest) return } runs, err := s.store.ListRunsByTask(r.Context(), taskID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, runs) }