From e19e735997d9af83924ba8e132a52208efcc873b Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:17:16 +0100 Subject: [PATCH] fixed sprites --- backend/assets_generate.go | 16 +- backend/assets_sprite.go | 30 +- backend/record_stream_mfc.go | 2 +- backend/recorder_settings.json | 16 +- backend/tasks_regenerate_assets.go | 376 ++++++++++++------ frontend/src/App.tsx | 4 +- frontend/src/components/ui/CookieModal.tsx | 22 +- .../src/components/ui/FinishedDownloads.tsx | 93 ++++- .../src/components/ui/RecorderSettings.tsx | 224 +++++++---- 9 files changed, 545 insertions(+), 238 deletions(-) diff --git a/backend/assets_generate.go b/backend/assets_generate.go index 1815882..d3c889d 100644 --- a/backend/assets_generate.go +++ b/backend/assets_generate.go @@ -489,7 +489,7 @@ func ensureSpritesForVideoCtx( return false, nil } - genCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + genCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() if err := genSem.Acquire(genCtx); err != nil { @@ -637,15 +637,21 @@ func ensureDeferredAssetsAndAI(ctx context.Context, videoPath, sourceURL string) return fmt.Errorf("deferred sprite failed for %s: %w", videoPath, err) } - truth := finishedPhaseTruthForID(id) - if !truth.SpritesReady { + spriteTruth := regeneratePhaseTruthForVideo(videoPath, id) + if !spriteTruth.SpritesReady { return fmt.Errorf("preview-sprite.jpg fehlt nach deferred generation: %s", id) } - if _, err := ensureAnalyzeForVideoCtx(ctx, videoPath, sourceURL, "nsfw"); err != nil { + analyzeReady, err := ensureAnalyzeForVideoCtx(ctx, videoPath, sourceURL, "nsfw") + if err != nil { return fmt.Errorf("deferred analyze failed for %s: %w", videoPath, err) } + analyzeTruth := regeneratePhaseTruthForVideo(videoPath, id) + if !analyzeReady && !analyzeTruth.AnalyzeReady { + return fmt.Errorf("KI-Analyse fehlt nach deferred analyze: %s", id) + } + return nil } @@ -995,7 +1001,7 @@ func ensureAssetsForVideoDetailed(ctx context.Context, videoPath string, sourceU return } - genCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + genCtx, cancel := context.WithTimeout(ctx, 4*time.Minute) defer cancel() if err := genSem.Acquire(genCtx); err != nil { diff --git a/backend/assets_sprite.go b/backend/assets_sprite.go index b83c135..66505dd 100644 --- a/backend/assets_sprite.go +++ b/backend/assets_sprite.go @@ -3,6 +3,7 @@ package main import ( + "bytes" "context" "fmt" "os" @@ -107,36 +108,45 @@ func generatePreviewSpriteJPG( cols, rows, ) - cmd := exec.CommandContext( - ctx, - ffmpegPath, + args := []string{ "-hide_banner", "-y", + } + args = append(args, ffmpegInputTol...) + args = append(args, "-i", videoPath, "-an", "-sn", "-threads", "0", "-vf", vf, "-frames:v", "1", + "-update", "1", "-c:v", "mjpeg", "-q:v", "4", - "-f", "image2", tmpPath, ) - out, err := cmd.CombinedOutput() + cmd := exec.CommandContext(ctx, ffmpegPath, args...) + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { - msg := strings.TrimSpace(string(out)) - if msg == "" { - msg = "(keine ffmpeg-ausgabe)" + msgErr := strings.TrimSpace(stderr.String()) + if msgErr == "" { + msgErr = "(kein stderr)" } + return fmt.Errorf( - "ffmpeg sprite failed for %q -> %q: %w | args=%q | output=%s", + "ffmpeg sprite failed for %q -> %q: %w | ctxErr=%v | args=%q | stderr=%s", videoPath, tmpPath, err, + ctx.Err(), strings.Join(cmd.Args, " "), - msg, + msgErr, ) } diff --git a/backend/record_stream_mfc.go b/backend/record_stream_mfc.go index 0efc2fd..6cdaa72 100644 --- a/backend/record_stream_mfc.go +++ b/backend/record_stream_mfc.go @@ -234,7 +234,7 @@ func runMFC(ctx context.Context, username string, outArg string) error { return err } if st != StatusPublic { - return fmt.Errorf("Stream ist nicht öffentlich (Status: %s)", st) + return fmt.Errorf("Stream ist nicht live (Status: %s)", st) } m3u8URL, err := mfc.GetVideoURL(false) diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index 11f7e48..b42d528 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -1,19 +1,19 @@ { "databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable", - "encryptedDbPassword": "7fOaXQbCfrAh++k3iuU8HVFIldwQ/xFTCGS5BY92/BiVk/NzkfgsxJU=", + "encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=", "recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records", "doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done", - "ffmpegPath": "C:\\ffmpeg\\ffmpeg.exe", - "autoAddToDownloadList": false, - "autoStartAddedDownloads": false, + "ffmpegPath": "", + "autoAddToDownloadList": true, + "autoStartAddedDownloads": true, "useChaturbateApi": true, "useMyFreeCamsWatcher": true, - "autoDeleteSmallDownloads": true, - "autoDeleteSmallDownloadsBelowMB": 200, + "autoDeleteSmallDownloads": false, + "autoDeleteSmallDownloadsBelowMB": 50, "lowDiskPauseBelowGB": 5, "blurPreviews": false, - "teaserPlayback": "hover", + "teaserPlayback": "all", "teaserAudio": true, "enableNotifications": true, - "encryptedCookies": "khmON0nSN9PsCC7Fb8btmhpmC9GmuQfWglOutI11SwrgM+EVs4SwkmcdOkPykPoWM+6oMqCYUCrCYF0ZHH18gVyZdiDC6GEpPhUQkjVYZJ8aOs4Gz0PSPQj89cMrQ9OBWVakaXAyFk1/m2xjez1ofA2uWlDREcSHPXhh24Gw9nICx23TkoKt5GJdy/+MH4qWI+qGI5zaYvr6dEO9iq4yDCpVgXeKPsylyRonDS0NuqYthjbay5P0A4Ir4Xr/66d4fp1+YLrEiWyASOsYvEmBhs9WO6/zNH/Uj4ZJqBlHaoCv1doaRBUy3XizWwh0Q71fGuGPrlqm8jjJBee3hbrgoC/22Ao6gC8+mLoG6ruqZNFjSecVSuDl/XTcIYgXqudUScLCemF40UT5rbs0aFH3awY4pm75K1AzL55lBHiSIb05UX/Rf1UUyP+ruFsGJBvHZkiOgAj8TpMoVNXpfTJQYDyZR6ISJHaFMfLZBUQ2U9Dim59Aon0nf6f84gsUhX2j3UsulkbtJxU=" + "encryptedCookies": "tZzqhwM+6rrdV36edfxpkhfikSfzfs4VZESLJIpYw1U4Bwn7vo+P8Sq86M7nvXpvbQVg00QLl5Wo2dD2agojU58GYY5P5P6Wpc1ZPr2T409BaQXRA/WhHiZv7z/b4JoVYX7fScZ4FDtscQYuTdMpnU6Wr725j66Qg2x8VgXqUHb1qceUes9P3L2kc8MAvZHWnZHIEjbgDW9NAZOADlbBxOonmc5hMGpz5xzmpWWV7Gqg6n1i75U/XBDV6Ks1Jn5Ic9dwi0f1BH6zo56v5oDdPAxKPlw01TUsV9ZAA+u+TjWUiq52AJP/C20bujUEsmQupqKxMs3E4rs6DEb7SYfbQ6SSryKyYYLCv0T3+CbeUgDmgYd2n+yjatE8udHvmkCtZCWvBIK4lSglsVeFq1prgpiGBL2ItyLcR13musHGbZ7Whq/p1btJsFDTTyfGnILW+sRUcd1Y/17YM5zKAWYyi81qKop6DYqh9kBoM2y3DYVxsLkb9+lUmwN6N7oopqh/bJ8vxJKyC74=" } diff --git a/backend/tasks_regenerate_assets.go b/backend/tasks_regenerate_assets.go index 5fa2e61..4e9e81e 100644 --- a/backend/tasks_regenerate_assets.go +++ b/backend/tasks_regenerate_assets.go @@ -20,6 +20,14 @@ type regenerateAssetsRequest struct { File string `json:"file"` } +type regenerateAssetsPhase struct { + Queue string `json:"queue"` + Phase string `json:"phase"` + State string `json:"state"` + Label string `json:"label,omitempty"` + TS int64 `json:"ts"` +} + type regenerateAssetsJob struct { File string VideoPath string @@ -28,12 +36,14 @@ type regenerateAssetsJob struct { Ctx context.Context Cancel context.CancelFunc - State string // queued | running | done | error | cancelled + State string Error string EnqueuedAt time.Time StartedAt time.Time EndedAt time.Time + + Phases map[string]regenerateAssetsPhase } var ( @@ -51,6 +61,8 @@ func publishFinishedPostworkPhase( state string, label string, ) { + setRegenerateAssetsJobPhase(file, queue, phase, state, label) + ev := finishedPostworkEvent{ Type: "finished_postwork", File: file, @@ -66,6 +78,83 @@ func publishFinishedPostworkPhase( } } +type regeneratePhaseTruth struct { + MetaReady bool + ThumbReady bool + TeaserReady bool + SpritesReady bool + AnalyzeReady bool +} + +func regeneratePhaseTruthForVideo(videoPath string, assetID string) regeneratePhaseTruth { + videoPath = strings.TrimSpace(videoPath) + assetID = strings.TrimSpace(assetID) + + if assetID == "" && videoPath != "" { + assetID = assetIDFromVideoPath(videoPath) + } + + var out regeneratePhaseTruth + if assetID == "" { + return out + } + + _, thumbPath, previewPath, spritePath, metaPath, err := assetPathsForID(assetID) + if err != nil { + return out + } + + fileExists := func(p string) bool { + fi, err := os.Stat(strings.TrimSpace(p)) + return err == nil && fi != nil && !fi.IsDir() && fi.Size() > 0 + } + + out.ThumbReady = fileExists(thumbPath) + out.TeaserReady = fileExists(previewPath) + out.SpritesReady = fileExists(spritePath) + + // Meta ist nur "ready", wenn die meta.json gültig zur aktuellen Videodatei passt + if videoPath != "" { + if vfi, err := os.Stat(videoPath); err == nil && vfi != nil && !vfi.IsDir() && vfi.Size() > 0 { + if meta, ok := readVideoMetaIfValid(metaPath, vfi); ok && meta != nil { + out.MetaReady = true + } + } + + // KI nur dann fertig, wenn Sprite existiert UND AI in meta.json vorhanden ist + out.AnalyzeReady = out.SpritesReady && hasAIResultsForOutput(videoPath) + } + + return out +} + +func firstMissingPrimaryRegeneratePhaseError(videoPath string, assetID string) error { + truth := regeneratePhaseTruthForVideo(videoPath, assetID) + + if !truth.MetaReady { + return errors.New("meta.json fehlt oder ist noch unvollständig") + } + if !truth.ThumbReady { + return errors.New("preview.jpg fehlt oder ist leer") + } + if !truth.TeaserReady { + return errors.New("preview.mp4 fehlt oder ist leer") + } + return nil +} + +func firstMissingDeferredRegeneratePhaseError(videoPath string, assetID string) error { + truth := regeneratePhaseTruthForVideo(videoPath, assetID) + + if !truth.SpritesReady { + return errors.New("preview-sprite.jpg fehlt oder ist leer") + } + if !truth.AnalyzeReady { + return errors.New("KI-Analyse fehlt oder wurde nicht in meta.json gespeichert") + } + return nil +} + func ensureRegenerateAssetsWorkersStarted() { regenerateAssetsMu.Lock() defer regenerateAssetsMu.Unlock() @@ -127,6 +216,34 @@ func finishRegenerateAssetsJob(file string, state string, errMsg string) { job.EndedAt = time.Now() } +func setRegenerateAssetsJobPhase( + file string, + queue string, + phase string, + state string, + label string, +) { + regenerateAssetsMu.Lock() + defer regenerateAssetsMu.Unlock() + + job := regenerateAssetsJobs[file] + if job == nil { + return + } + if job.Phases == nil { + job.Phases = make(map[string]regenerateAssetsPhase) + } + + key := strings.ToLower(strings.TrimSpace(queue)) + ":" + strings.ToLower(strings.TrimSpace(phase)) + job.Phases[key] = regenerateAssetsPhase{ + Queue: strings.TrimSpace(queue), + Phase: strings.TrimSpace(phase), + State: strings.TrimSpace(state), + Label: strings.TrimSpace(label), + TS: time.Now().UnixMilli(), + } +} + func removeRegenerateAssetsJobLater(file string, after time.Duration) { time.AfterFunc(after, func() { regenerateAssetsMu.Lock() @@ -142,11 +259,23 @@ func getRegenerateAssetsJobsSnapshot() []map[string]any { out := make([]map[string]any, 0, len(regenerateAssetsJobs)) for _, job := range regenerateAssetsJobs { + phases := make([]map[string]any, 0, len(job.Phases)) + for _, p := range job.Phases { + phases = append(phases, map[string]any{ + "queue": p.Queue, + "phase": p.Phase, + "state": p.State, + "label": p.Label, + "ts": p.TS, + }) + } + out = append(out, map[string]any{ "file": job.File, "assetId": job.AssetID, "state": job.State, "error": job.Error, + "phases": phases, "enqueuedAt": job.EnqueuedAt.UnixMilli(), "startedAt": func() int64 { if job.StartedAt.IsZero() { @@ -185,32 +314,44 @@ func runRegenerateAssetsJob(job *regenerateAssetsJob) { ctx := job.Ctx setRegenerateAssetsTaskRunning(file, file) - cancelDeferredEnrichForFile(file) - select { - case <-ctx.Done(): + cancelJob := func() { finishRegenerateAssetsJob(file, "cancelled", "Abgebrochen") setRegenerateAssetsTaskError(file, "Abgebrochen") publishFinishedPostworkPhase(file, id, "postwork", "meta", "missing", "") - publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "") publishFinishedPostworkPhase(file, id, "postwork", "thumb", "missing", "") + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "") publishFinishedPostworkPhase(file, id, "postwork", "sprites", "missing", "") publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "") removeRegenerateAssetsJobLater(file, 2*time.Second) - return - default: } - publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "") + failJob := func(queue, phase, msg string) { + if queue != "" && phase != "" { + publishFinishedPostworkPhase(file, id, queue, phase, "error", "Fehler") + } + finishRegenerateAssetsJob(file, "error", msg) + setRegenerateAssetsTaskError(file, msg) + removeRegenerateAssetsJobLater(file, 5*time.Second) + } + + if ctx.Err() == context.Canceled { + cancelJob() + return + } + + // Initialstatus publishFinishedPostworkPhase(file, id, "postwork", "meta", "running", "Meta") - publishFinishedPostworkPhase(file, id, "postwork", "teaser", "running", "Teaser") - publishFinishedPostworkPhase(file, id, "postwork", "thumb", "running", "Vorschaubild") - publishFinishedPostworkPhase(file, id, "postwork", "sprites", "running", "Sprites") + publishFinishedPostworkPhase(file, id, "postwork", "thumb", "queued", "Vorschaubild") + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "queued", "Teaser") + publishFinishedPostworkPhase(file, id, "postwork", "sprites", "queued", "Sprites") + publishFinishedPostworkPhase(file, id, "enrich", "analyze", "queued", "KI-Analyse") + // Alte Assets löschen if id != "" { if assetDir, _, _, _, metaPath, err := assetPathsForID(id); err == nil { _ = os.Remove(filepath.Join(assetDir, "preview.jpg")) @@ -220,116 +361,99 @@ func runRegenerateAssetsJob(job *regenerateAssetsJob) { } } - assetWatchDone := make(chan struct{}) - - go func(ctx context.Context, file string, id string) { - ticker := time.NewTicker(300 * time.Millisecond) - defer ticker.Stop() - - sentMetaDone := false - sentTeaserDone := false - sentThumbDone := false - sentSpritesDone := false - - checkAndEmit := func() { - truth := finishedPhaseTruthForID(id) - - if truth.MetaReady && !sentMetaDone { - sentMetaDone = true - publishFinishedPostworkPhase(file, id, "postwork", "meta", "done", "Meta") - } - if truth.TeaserReady && !sentTeaserDone { - sentTeaserDone = true - publishFinishedPostworkPhase(file, id, "postwork", "teaser", "done", "Teaser") - } - if truth.ThumbReady && !sentThumbDone { - sentThumbDone = true - publishFinishedPostworkPhase(file, id, "postwork", "thumb", "done", "Vorschaubild") - } - if truth.SpritesReady && !sentSpritesDone { - sentSpritesDone = true - publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites") - } + // 1) Meta + if _, err := ensureMetaForVideoCtx(ctx, videoPath, ""); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled { + cancelJob() + return } - - for { - select { - case <-ctx.Done(): - return - case <-assetWatchDone: - checkAndEmit() - return - case <-ticker.C: - checkAndEmit() - } - } - }(ctx, file, id) - - _, errAssets := ensureAssetsForVideoWithProgressCtx(ctx, videoPath, "", nil) - close(assetWatchDone) - - if errors.Is(errAssets, context.Canceled) || ctx.Err() == context.Canceled { - finishRegenerateAssetsJob(file, "cancelled", "Abgebrochen") - setRegenerateAssetsTaskError(file, "Abgebrochen") - - publishFinishedPostworkPhase(file, id, "postwork", "meta", "missing", "") - publishFinishedPostworkPhase(file, id, "postwork", "teaser", "missing", "") - publishFinishedPostworkPhase(file, id, "postwork", "thumb", "missing", "") - publishFinishedPostworkPhase(file, id, "postwork", "sprites", "missing", "") - publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "") - - removeRegenerateAssetsJobLater(file, 2*time.Second) + failJob("postwork", "meta", err.Error()) return } - if errAssets == nil { - publishFinishedPostworkPhase(file, id, "enrich", "analyze", "running", "KI-Analyse") - } - - errAI := ensureDeferredAssetsAndAI(ctx, videoPath, "") - - if errors.Is(errAI, context.Canceled) || ctx.Err() == context.Canceled { - finishRegenerateAssetsJob(file, "cancelled", "Abgebrochen") - setRegenerateAssetsTaskError(file, "Abgebrochen") - publishFinishedPostworkPhase(file, id, "enrich", "analyze", "missing", "") - - removeRegenerateAssetsJobLater(file, 2*time.Second) + if !regeneratePhaseTruthForVideo(videoPath, id).MetaReady { + failJob("postwork", "meta", "meta.json fehlt oder ist noch unvollständig") return } - if errAssets == nil && errAI == nil { - publishFinishedPostworkPhase(file, id, "enrich", "analyze", "done", "KI-Analyse") - } + publishFinishedPostworkPhase(file, id, "postwork", "meta", "done", "Meta") - if errAssets != nil || errAI != nil { - errMsg := "" - if errAssets != nil { - errMsg = errAssets.Error() - } else { - errMsg = errAI.Error() + // 2) Thumb + publishFinishedPostworkPhase(file, id, "postwork", "thumb", "running", "Vorschaubild") + + if _, err := ensurePrimaryAssetsForVideoWithProgressCtx(ctx, videoPath, "", nil); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled { + cancelJob() + return } - - finishRegenerateAssetsJob(file, "error", errMsg) - setRegenerateAssetsTaskError(file, errMsg) - - errEv := finishedPostworkEvent{ - Type: "finished_postwork", - File: file, - AssetID: id, - Queue: "enrich", - State: "error", - Phase: "assets", - Label: "Fehler", - TS: time.Now().UnixMilli(), - } - if b, err := json.Marshal(errEv); err == nil { - publishSSE("finishedPostwork", b) - } - - removeRegenerateAssetsJobLater(file, 5*time.Second) + failJob("postwork", "thumb", err.Error()) return } + if !regeneratePhaseTruthForVideo(videoPath, id).ThumbReady { + failJob("postwork", "thumb", "preview.jpg fehlt oder ist leer") + return + } + + publishFinishedPostworkPhase(file, id, "postwork", "thumb", "done", "Vorschaubild") + + // 3) Teaser + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "running", "Teaser") + + if _, err := ensureTeaserForVideoCtx(ctx, videoPath, ""); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled { + cancelJob() + return + } + failJob("postwork", "teaser", err.Error()) + return + } + + if !regeneratePhaseTruthForVideo(videoPath, id).TeaserReady { + failJob("postwork", "teaser", "preview.mp4 fehlt oder ist leer") + return + } + + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "done", "Teaser") + + // 4) Sprites + publishFinishedPostworkPhase(file, id, "postwork", "sprites", "running", "Sprites") + + if _, err := ensureSpritesForVideoCtx(ctx, videoPath, ""); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled { + cancelJob() + return + } + failJob("postwork", "sprites", err.Error()) + return + } + + if !regeneratePhaseTruthForVideo(videoPath, id).SpritesReady { + failJob("postwork", "sprites", "preview-sprite.jpg fehlt oder ist leer") + return + } + + publishFinishedPostworkPhase(file, id, "postwork", "sprites", "done", "Sprites") + + // 5) KI-Analyse + publishFinishedPostworkPhase(file, id, "enrich", "analyze", "running", "KI-Analyse") + + if _, err := ensureAnalyzeForVideoCtx(ctx, videoPath, "", "nsfw"); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled { + cancelJob() + return + } + failJob("enrich", "analyze", err.Error()) + return + } + + if !regeneratePhaseTruthForVideo(videoPath, id).AnalyzeReady { + failJob("enrich", "analyze", "KI-Analyse fehlt oder wurde nicht in meta.json gespeichert") + return + } + + publishFinishedPostworkPhase(file, id, "enrich", "analyze", "done", "KI-Analyse") + finishRegenerateAssetsJob(file, "done", "") setRegenerateAssetsTaskDone(file, "Fertig") removeRegenerateAssetsJobLater(file, 3*time.Second) @@ -346,6 +470,18 @@ func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&req) file := strings.TrimSpace(req.File) + + if file == "" { + regenerateAssetsMu.Lock() + for _, j := range regenerateAssetsJobs { + if j != nil && (j.State == "running" || j.State == "queued") { + file = j.File + break + } + } + regenerateAssetsMu.Unlock() + } + if file == "" { http.Error(w, "missing file", http.StatusBadRequest) return @@ -412,10 +548,23 @@ func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) { regenerateAssetsMu.Unlock() w.Header().Set("Content-Type", "application/json") + phases := make([]map[string]any, 0, len(existing.Phases)) + for _, p := range existing.Phases { + phases = append(phases, map[string]any{ + "queue": p.Queue, + "phase": p.Phase, + "state": p.State, + "label": p.Label, + "ts": p.TS, + }) + } + _ = json.NewEncoder(w).Encode(map[string]any{ - "ok": true, - "file": file, - "state": existing.State, + "ok": true, + "file": file, + "state": existing.State, + "error": existing.Error, + "phases": phases, }) return } @@ -436,13 +585,14 @@ func handleRegenerateAssets(w http.ResponseWriter, r *http.Request) { Cancel: runCancel, State: "queued", EnqueuedAt: time.Now(), + Phases: make(map[string]regenerateAssetsPhase), } enqueueRegenerateAssetsJob(job) publishFinishedPostworkPhase(file, id, "postwork", "meta", "queued", "Meta") - publishFinishedPostworkPhase(file, id, "postwork", "teaser", "queued", "Teaser") publishFinishedPostworkPhase(file, id, "postwork", "thumb", "queued", "Vorschaubild") + publishFinishedPostworkPhase(file, id, "postwork", "teaser", "queued", "Teaser") publishFinishedPostworkPhase(file, id, "postwork", "sprites", "queued", "Sprites") publishFinishedPostworkPhase(file, id, "enrich", "analyze", "queued", "KI-Analyse") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5698bad..4e1da91 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -221,7 +221,7 @@ function normalizeRoomStatusText(v: unknown): 'public' | 'private' | 'hidden' | function roomStatusLabelDE(v: unknown): string { switch (normalizeRoomStatusText(v)) { case 'public': - return 'öffentlich' + return 'live' case 'private': return 'privat' case 'hidden': @@ -1207,7 +1207,7 @@ export default function App() { const title = nextStatus === 'public' - ? `${displayName} ist jetzt öffentlich` + ? `${displayName} ist live` : `${displayName} Status geändert` const message = `Status: ${roomStatusLabelDE(prevStatus)} → ${roomStatusLabelDE(nextStatus)}` diff --git a/frontend/src/components/ui/CookieModal.tsx b/frontend/src/components/ui/CookieModal.tsx index 2a7a3ae..258f6fa 100644 --- a/frontend/src/components/ui/CookieModal.tsx +++ b/frontend/src/components/ui/CookieModal.tsx @@ -59,7 +59,7 @@ export default function CookieModal({ } return ( - +