From f309a390274059a84de8846e98d95cff78d2994f Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 3 Oct 2026 21:06:46 +0200 Subject: [PATCH] feat(web): POST /uploads/{id}/delete, owner-scoped and refused while the recording is active (#37) Co-Authored-By: Claude Sonnet 5.5 --- internal/web/handlers.go | 4 ++++ internal/web/uploads.go | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/web/handlers.go b/internal/web/handlers.go index 141120a..4175757 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -59,6 +59,9 @@ type Store interface { // non-owner's id is ErrUploadNotFound. GetUpload(ctx context.Context, userID, id string) (*domain.UploadedTranscript, error) ListUploads(ctx context.Context, userID string) ([]domain.UploadedTranscript, error) + // DeleteUpload removes a finished upload; ErrUploadInProgress while it is still being + // processed, ErrUploadNotFound for a non-owner. + DeleteUpload(ctx context.Context, userID, id string) error // SetTranscriptStatus clears or updates a video's transcript backoff state. // Used by handleRetryNow to clear rate_limited_at before immediate processing. @@ -200,6 +203,7 @@ func (a *App) Router() http.Handler { app.HandleFunc("POST /uploads", a.handleUploadPost) app.HandleFunc("GET /uploads/{id}", a.handleUploadDetail) app.HandleFunc("GET /uploads/{id}/status", a.handleUploadStatus) + app.HandleFunc("POST /uploads/{id}/delete", a.handleUploadDelete) // Chat over the uploaded transcript reuses the ADR-027 Chatter; mounted only // when one is wired. if a.Chat != nil { diff --git a/internal/web/uploads.go b/internal/web/uploads.go index d35ee3f..f4638f6 100644 --- a/internal/web/uploads.go +++ b/internal/web/uploads.go @@ -210,6 +210,32 @@ func (a *App) handleUploadStatus(w http.ResponseWriter, r *http.Request) { a.render(w, r, uploadStatus(*u, a.Chat != nil, a.now())) } +// handleUploadDelete removes one of the caller's finished recordings and returns to the +// list. Another user's id is a 404 like every other upload route; a recording still being +// transcribed is refused (409) rather than deleted under the worker (#37). +func (a *App) handleUploadDelete(w http.ResponseWriter, r *http.Request) { + userID, ok := a.currentUserID(w, r) + if !ok { + return + } + id := r.PathValue("id") + if !uuidShape.MatchString(id) { + http.NotFound(w, r) + return + } + switch err := a.Store.DeleteUpload(r.Context(), userID, id); { + case err == nil: + http.Redirect(w, r, "/uploads", http.StatusSeeOther) + case errors.Is(err, store.ErrUploadNotFound): + http.NotFound(w, r) + case errors.Is(err, store.ErrUploadInProgress): + a.renderUploadsPage(w, r, userID, http.StatusConflict, + "This recording is still being transcribed. You can delete it once it has finished.") + default: + a.serverError(w, r, "delete upload", err) + } +} + func (a *App) loadOwnedUpload(w http.ResponseWriter, r *http.Request) (*domain.UploadedTranscript, bool) { userID, ok := a.currentUserID(w, r) if !ok {