// backend\tasks_status.go package main import ( "encoding/json" "fmt" "net/http" "strings" "sync" "sync/atomic" "time" ) type CleanupTaskState struct { ID string `json:"id"` Queued bool `json:"queued"` Running bool `json:"running"` QueuedAt time.Time `json:"queuedAt"` StartedAt time.Time `json:"startedAt"` FinishedAt *time.Time `json:"finishedAt,omitempty"` Text string `json:"text,omitempty"` Error string `json:"error,omitempty"` CurrentFile string `json:"currentFile,omitempty"` Done int `json:"done"` Total int `json:"total"` } type CheckVideosTaskState struct { ID string `json:"id"` Queued bool `json:"queued"` Running bool `json:"running"` QueuedAt time.Time `json:"queuedAt"` StartedAt time.Time `json:"startedAt"` FinishedAt *time.Time `json:"finishedAt,omitempty"` Text string `json:"text,omitempty"` Error string `json:"error,omitempty"` CurrentFile string `json:"currentFile,omitempty"` Done int `json:"done"` Total int `json:"total"` } type RegenerateAssetsTaskState struct { Running bool `json:"running"` File string `json:"file,omitempty"` Text string `json:"text,omitempty"` Error string `json:"error,omitempty"` } type TasksStatusResponse struct { GenerateAssetsJobs []AssetsTaskState `json:"generateAssetsJobs"` RegenerateAssetsJobs []map[string]any `json:"regenerateAssetsJobs"` CheckVideosJobs []CheckVideosTaskState `json:"checkVideosJobs"` CleanupJobs []CleanupTaskState `json:"cleanupJobs"` AnyRunning bool `json:"anyRunning"` } var ( cleanupTaskMu sync.Mutex cleanupTaskState CleanupTaskState regenerateAssetsTaskMu sync.Mutex regenerateAssetsTaskState RegenerateAssetsTaskState ) var nextTaskID atomic.Uint64 func newTaskID(prefix string) string { n := nextTaskID.Add(1) return fmt.Sprintf("%s-%d-%d", prefix, time.Now().UnixNano(), n) } func cleanupNowTime() *time.Time { t := time.Now() return &t } func anyToString(v any) string { switch x := v.(type) { case string: return x case []byte: return string(x) default: return "" } } func snapshotRegenerateAssetsTaskState() RegenerateAssetsTaskState { regenerateAssetsTaskMu.Lock() st := regenerateAssetsTaskState regenerateAssetsTaskMu.Unlock() return st } func setRegenerateAssetsTaskRunning(file, text string) { regenerateAssetsTaskMu.Lock() regenerateAssetsTaskState = RegenerateAssetsTaskState{ Running: true, File: strings.TrimSpace(file), Text: strings.TrimSpace(text), Error: "", } regenerateAssetsTaskMu.Unlock() publishTaskState() } func setRegenerateAssetsTaskDone(file, text string) { regenerateAssetsTaskMu.Lock() regenerateAssetsTaskState = RegenerateAssetsTaskState{ Running: false, File: strings.TrimSpace(file), Text: strings.TrimSpace(text), Error: "", } regenerateAssetsTaskMu.Unlock() publishTaskState() } func setRegenerateAssetsTaskError(file, errText string) { regenerateAssetsTaskMu.Lock() regenerateAssetsTaskState = RegenerateAssetsTaskState{ Running: false, File: strings.TrimSpace(file), Text: "", Error: strings.TrimSpace(errText), } regenerateAssetsTaskMu.Unlock() publishTaskState() } func clearRegenerateAssetsTaskState() { regenerateAssetsTaskMu.Lock() regenerateAssetsTaskState = RegenerateAssetsTaskState{} regenerateAssetsTaskMu.Unlock() publishTaskState() } func snapshotCleanupTaskState() CleanupTaskState { cleanupTaskMu.Lock() st := cleanupTaskState cleanupTaskMu.Unlock() return st } func setCleanupTaskQueued(text string) { cleanupTaskMu.Lock() cleanupTaskState = CleanupTaskState{ Queued: true, Running: false, QueuedAt: time.Now(), StartedAt: time.Time{}, FinishedAt: nil, Text: strings.TrimSpace(text), Error: "", CurrentFile: "", Done: 0, Total: 0, } cleanupTaskMu.Unlock() publishTaskState() } func setCleanupTaskRunning(text string) { cleanupTaskMu.Lock() now := time.Now() queuedAt := cleanupTaskState.QueuedAt if queuedAt.IsZero() { queuedAt = now } cleanupTaskState = CleanupTaskState{ Queued: false, Running: true, QueuedAt: queuedAt, StartedAt: now, FinishedAt: nil, Text: strings.TrimSpace(text), Error: "", CurrentFile: "", Done: 0, Total: 0, } cleanupTaskMu.Unlock() publishTaskState() } func setCleanupTaskProgress(done, total int, currentFile, text string) { cleanupTaskMu.Lock() now := time.Now() if cleanupTaskState.QueuedAt.IsZero() { cleanupTaskState.QueuedAt = now } if cleanupTaskState.StartedAt.IsZero() { cleanupTaskState.StartedAt = now } cleanupTaskState.Queued = false cleanupTaskState.Running = true cleanupTaskState.Done = done cleanupTaskState.Total = total cleanupTaskState.CurrentFile = strings.TrimSpace(currentFile) cleanupTaskState.Text = strings.TrimSpace(text) cleanupTaskState.Error = "" cleanupTaskState.FinishedAt = nil cleanupTaskMu.Unlock() publishTaskState() } func setCleanupTaskDone(text string) { cleanupTaskMu.Lock() cleanupTaskState.Queued = false cleanupTaskState.Running = false cleanupTaskState.Text = strings.TrimSpace(text) cleanupTaskState.CurrentFile = "" cleanupTaskState.Error = "" cleanupTaskState.FinishedAt = cleanupNowTime() if cleanupTaskState.Total > 0 { cleanupTaskState.Done = cleanupTaskState.Total } cleanupTaskMu.Unlock() publishTaskState() } func setCleanupTaskError(errText string) { cleanupTaskMu.Lock() cleanupTaskState.Queued = false cleanupTaskState.Running = false cleanupTaskState.Text = "" cleanupTaskState.Error = strings.TrimSpace(errText) cleanupTaskState.CurrentFile = "" cleanupTaskState.FinishedAt = cleanupNowTime() cleanupTaskMu.Unlock() publishTaskState() } func tasksStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Nur GET erlaubt", http.StatusMethodNotAllowed) return } assetsJobs := snapshotAssetsJobs() regenerateJobs := getRegenerateAssetsJobsSnapshot() checkVideosJobs := snapshotCheckVideosJobs() cleanupJobs := snapshotCleanupJobs() anyRunning := false if !anyRunning { for _, st := range assetsJobs { if st.Queued || st.Running { anyRunning = true break } } } if !anyRunning { for _, st := range checkVideosJobs { if st.Queued || st.Running { anyRunning = true break } } } if !anyRunning { for _, st := range cleanupJobs { if st.Queued || st.Running { anyRunning = true break } } } if !anyRunning { for _, job := range regenerateJobs { state := strings.TrimSpace(strings.ToLower(anyToString(job["state"]))) if state == "queued" || state == "running" { anyRunning = true break } } } resp := TasksStatusResponse{ GenerateAssetsJobs: assetsJobs, RegenerateAssetsJobs: regenerateJobs, CheckVideosJobs: checkVideosJobs, CleanupJobs: cleanupJobs, AnyRunning: anyRunning, } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(resp) }