From 404f74c55ccd02e6d942545ea47691b1c80ad151 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 3 Jun 2026 19:46:20 +0200 Subject: [PATCH] feat(web): Processor port + in-flight ProcessingSet on App MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the seam for immediate web-triggered summarization. Processor is the optional single-video summarize port (nil = queue-only, unchanged behaviour); ProcessingSet is an ephemeral, concurrency-safe set of in-flight (user,video) ids so a status endpoint can show progress until the summary lands. State is deliberately in-memory only — the DB holds the durable truth across restarts. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/web/handlers.go | 7 ++++++ internal/web/processing.go | 42 +++++++++++++++++++++++++++++++++ internal/web/processing_test.go | 27 +++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 internal/web/processing.go create mode 100644 internal/web/processing_test.go diff --git a/internal/web/handlers.go b/internal/web/handlers.go index 41e8908..cd61b0f 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -61,6 +61,13 @@ type App struct { // Secrets removes a user's OAuth tokens on disconnect / delete-account. The // account routes require it; cmd/tapir wires the file-backed store. Secrets SecretRemover + // Processor, when non-nil, summarizes a queued video immediately in a + // background goroutine (the "Summarize" button kicks it off). Nil = queue-only: + // the button flips the DB flag and the next `tapir run` does the work. + Processor Processor + // Processing tracks in-flight immediate summarizations so the status endpoint + // shows the animation until the summary lands. The zero value is ready to use. + Processing ProcessingSet } func (a *App) logger() *slog.Logger { diff --git a/internal/web/processing.go b/internal/web/processing.go new file mode 100644 index 0000000..0ed2609 --- /dev/null +++ b/internal/web/processing.go @@ -0,0 +1,42 @@ +package web + +import ( + "context" + "sync" +) + +// Processor runs the core summarization use case for a single already-discovered +// video — resolve its transcript, summarize, deliver to the store. *usecase.Engine +// wrapped with the store satisfies it (wired in cmd/tapir). Optional on App: a nil +// Processor means queue-only — the "Summarize" button only flips the DB flag and +// the next `tapir run` does the work. +type Processor interface { + ProcessVideo(ctx context.Context, userID, videoID string) error +} + +// ProcessingSet tracks the (user, video) ids currently being summarized in-process +// so the status endpoint can show the animation until the summary lands. It is +// ephemeral (single-instance Stage-1): a restart drops it, and the DB holds the +// durable state — the summary is present, or summarize_requested is still set so +// `tapir run` retries. The zero value is ready to use; methods are concurrency-safe. +type ProcessingSet struct { + m sync.Map +} + +// Add marks a key in-flight. +func (p *ProcessingSet) Add(key string) { p.m.Store(key, struct{}{}) } + +// Remove clears a key once its summarization finishes (success or failure). +func (p *ProcessingSet) Remove(key string) { p.m.Delete(key) } + +// Has reports whether a key is currently in-flight. +func (p *ProcessingSet) Has(key string) bool { + _, ok := p.m.Load(key) + return ok +} + +// processingKey scopes the in-flight key by user so one user's summarization is +// never confused with another's for the same video id. +func processingKey(userID, videoID string) string { + return userID + "|" + videoID +} diff --git a/internal/web/processing_test.go b/internal/web/processing_test.go new file mode 100644 index 0000000..7bb07f4 --- /dev/null +++ b/internal/web/processing_test.go @@ -0,0 +1,27 @@ +package web + +import "testing" + +func TestProcessingSetAddHasRemove(t *testing.T) { + var s ProcessingSet // zero value is usable + + key := processingKey("user-1", "video-1") + if s.Has(key) { + t.Fatal("fresh set must not report a key as in-flight") + } + + s.Add(key) + if !s.Has(key) { + t.Fatal("Add must mark the key in-flight") + } + + // A different user with the same video id is a distinct key. + if s.Has(processingKey("user-2", "video-1")) { + t.Fatal("keys must be scoped by user") + } + + s.Remove(key) + if s.Has(key) { + t.Fatal("Remove must clear the key") + } +}