feat(backend): per-file review status with DSL filter and bulk endpoint

Replaces the old "untagged" sentinel tag with a proper per-file workflow
status: needs_review starts true on upload/import and is cleared by an
explicit action (no auto-clear on tagging). Surfaced as a filter token
(r=1 needs review, r=0 done) so it combines with tag/MIME conditions, and
toggled via POST /files/bulk/review (single id or many, edit-ACL enforced,
audit-logged as file_review).

needs_review lives on data.files (column added to the original 003 migration,
partial index in 006, action type seeded in 007).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 21:16:47 +03:00
parent 4d11beb296
commit 48e901cac1
12 changed files with 215 additions and 9 deletions
+29
View File
@@ -84,6 +84,7 @@ type fileJSON struct {
CreatorName string `json:"creator_name"`
IsPublic bool `json:"is_public"`
IsDeleted bool `json:"is_deleted"`
NeedsReview bool `json:"needs_review"`
CreatedAt string `json:"created_at"`
Tags []tagJSON `json:"tags"`
}
@@ -131,6 +132,7 @@ func toFileJSON(f domain.File) fileJSON {
CreatorName: f.CreatorName,
IsPublic: f.IsPublic,
IsDeleted: f.IsDeleted,
NeedsReview: f.NeedsReview,
CreatedAt: f.CreatedAt.Format(time.RFC3339),
Tags: tags,
}
@@ -661,6 +663,33 @@ func (h *FileHandler) BulkDelete(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// BulkReview sets the review status on one or more files. A single-file toggle
// is just a one-element file_ids array. Files the caller cannot edit are
// silently skipped (handled in the service).
func (h *FileHandler) BulkReview(c *gin.Context) {
var body struct {
FileIDs []string `json:"file_ids" binding:"required"`
NeedsReview *bool `json:"needs_review" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.NeedsReview == nil {
respondError(c, domain.ErrValidation)
return
}
fileIDs, err := parseUUIDs(body.FileIDs)
if err != nil {
respondError(c, domain.ErrValidation)
return
}
if err := h.fileSvc.SetNeedsReview(c.Request.Context(), fileIDs, *body.NeedsReview); err != nil {
respondError(c, err)
return
}
c.Status(http.StatusNoContent)
}
// ---------------------------------------------------------------------------
// POST /files/bulk/common-tags
// ---------------------------------------------------------------------------
+1
View File
@@ -83,6 +83,7 @@ func NewRouter(
// Bulk + import routes registered before /:id to prevent param collision.
files.POST("/bulk/tags", fileHandler.BulkSetTags)
files.POST("/bulk/delete", fileHandler.BulkDelete)
files.POST("/bulk/review", fileHandler.BulkReview)
files.POST("/bulk/common-tags", fileHandler.CommonTags)
files.POST("/import", fileHandler.Import)