feat(web): immediate summarize + status poll + ASCII tapir spinner
Clicking "Summarize" now runs the summary in the background (when a Processor is
wired) instead of only queuing it. The handler flips the DB flag, kicks off the
work on a detached context, and returns an animated "processing" card that polls
GET /v/{id}/status every 2s via HTMX. Status returns the summary card once it
lands (no poll → polling stops), the animation while in-flight, or the Queued
card otherwise. Queue-only behaviour is unchanged when no Processor is set.
The spinner is a CSS-only cross-fade of three ASCII tapir frames — no JS, honours
prefers-reduced-motion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,7 @@ func (a *App) Router() http.Handler {
|
||||
app.HandleFunc("GET /v/{videoId}", a.handleDetail)
|
||||
app.HandleFunc("POST /v/{videoId}/action", a.handleAction)
|
||||
app.HandleFunc("POST /v/{videoId}/summarize", a.handleRequestSummarize)
|
||||
app.HandleFunc("GET /v/{videoId}/status", a.handleStatus)
|
||||
app.HandleFunc("GET /register", a.handleRegisterForm)
|
||||
app.HandleFunc("POST /register", a.handleRegister)
|
||||
|
||||
@@ -215,10 +216,12 @@ func (a *App) handleAction(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/v/"+videoID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleRequestSummarize queues a video for manual summarization. It does NOT run
|
||||
// the engine inline — it only flips summarize_requested; the next `tapir run`
|
||||
// picks it up (the single summarization driver). For HTMX it returns the refreshed
|
||||
// card (now showing "Queued"); without JS it redirects back to the list.
|
||||
// handleRequestSummarize handles the "Summarize" button. It always flips the DB
|
||||
// flag (summarize_requested) so the work is durable. With a Processor wired it
|
||||
// then summarizes immediately in the background and answers with the animated
|
||||
// processing card that polls /status until done; without one (queue-only) it
|
||||
// answers with the "Queued" card — the next `tapir run` does the work. Without
|
||||
// JS it redirects back to the list (POST→redirect→GET).
|
||||
func (a *App) handleRequestSummarize(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := a.currentUserID(w, r)
|
||||
if !ok {
|
||||
@@ -245,9 +248,60 @@ func (a *App) handleRequestSummarize(w http.ResponseWriter, r *http.Request) {
|
||||
a.serverError(w, r, "get video", err)
|
||||
return
|
||||
}
|
||||
|
||||
if a.Processor != nil {
|
||||
a.startProcessing(userID, videoID)
|
||||
a.render(w, r, processingCard(*row))
|
||||
return
|
||||
}
|
||||
a.render(w, r, VideoCard(*row))
|
||||
}
|
||||
|
||||
// startProcessing marks a video in-flight and summarizes it in the background.
|
||||
// The goroutine uses a detached context — not the request's, which is cancelled
|
||||
// when the handler returns — and clears the in-flight mark on completion. On
|
||||
// error the DB flag stays set, so the video remains queued for the next
|
||||
// `tapir run`; a successful Processor.ProcessVideo clears it itself.
|
||||
func (a *App) startProcessing(userID, videoID string) {
|
||||
key := processingKey(userID, videoID)
|
||||
a.Processing.Add(key)
|
||||
go func() {
|
||||
defer a.Processing.Remove(key)
|
||||
if err := a.Processor.ProcessVideo(context.Background(), userID, videoID); err != nil {
|
||||
a.logger().Error("background summarize", "user", userID, "video", videoID, "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleStatus is the HTMX poll target for an in-flight summarization. It returns
|
||||
// the card in its current state: the full summary card once the summary exists,
|
||||
// otherwise the animated processing card while still in-flight (which keeps
|
||||
// polling), or the queued/button card when neither holds. VideoCard carries no
|
||||
// polling attributes, so HTMX stops polling once it swaps in.
|
||||
func (a *App) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := a.currentUserID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
videoID := r.PathValue("videoId")
|
||||
|
||||
row, err := a.Store.GetVideoRow(r.Context(), userID, videoID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.serverError(w, r, "get video", err)
|
||||
return
|
||||
}
|
||||
|
||||
if row.Summarized || !a.Processing.Has(processingKey(userID, videoID)) {
|
||||
a.render(w, r, VideoCard(*row))
|
||||
return
|
||||
}
|
||||
a.render(w, r, processingCard(*row))
|
||||
}
|
||||
|
||||
// handleSummarizeMode toggles the user's auto/manual summarization mode. The form
|
||||
// submits the desired new value (enabled=true|false). For HTMX it returns the
|
||||
// refreshed mode control; without JS it redirects back to the account page.
|
||||
|
||||
Reference in New Issue
Block a user