From 492892097dd81449bd5a5fccd7b025c88460e831 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 12 Apr 2026 20:43:57 -0400 Subject: [PATCH] feat(admin): Add processing issues management UI and API - Add ProcessingIssuesHandler with List and GetStats methods - Add AdminProcessingIssues template for issues dashboard - Display error/warning/info stats cards - Sort issues by severity and creation date - Add dismiss functionality for warnings and info items - Add navigate to media item functionality - Show issue type, description, and media details --- internal/handlers/processing_issues.go | 139 ++++++++++++++++++++++++ templates/admin_processing_issues.templ | 95 ++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 internal/handlers/processing_issues.go create mode 100644 templates/admin_processing_issues.templ diff --git a/internal/handlers/processing_issues.go b/internal/handlers/processing_issues.go new file mode 100644 index 0000000..3ba57ae --- /dev/null +++ b/internal/handlers/processing_issues.go @@ -0,0 +1,139 @@ +package handlers + +import ( + "bookhoard/internal/database" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v5" +) + +type ProcessingIssuesHandler struct { + db *database.Queries +} + +func NewProcessingIssuesHandler(db *database.Queries) *ProcessingIssuesHandler { + return &ProcessingIssuesHandler{db: db} +} + +type ProcessingIssueResponse struct { + ID string `json:"id"` + MediaItemID string `json:"media_item_id"` + Title string `json:"title"` + FilePath string `json:"file_path"` + FormatGroup string `json:"format_group"` + LibraryTypeName string `json:"library_type_name"` + IssueType string `json:"issue_type"` + IssueDescription string `json:"issue_description"` + Severity string `json:"severity"` + CreatedAt time.Time `json:"created_at"` +} + +type ProcessingIssueStats struct { + ErrorCount int64 `json:"error_count"` + WarningCount int64 `json:"warning_count"` + InfoCount int64 `json:"info_count"` +} + +// ListProcessingIssues returns all processing issues for a library +func (h *ProcessingIssuesHandler) ListProcessingIssues(c *echo.Context) error { + libraryID, err := uuid.Parse(c.Param("libraryId")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid library ID"}) + } + + issues, err := h.db.ListProcessingIssuesByLibrary( + c.Request().Context(), + pgtype.UUID{Bytes: libraryID, Valid: true}, + ) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch issues"}) + } + + response := make([]ProcessingIssueResponse, len(issues)) + for i, issue := range issues { + response[i] = ProcessingIssueResponse{ + ID: issue.ID.String(), + MediaItemID: issue.MediaItemID.String(), + Title: issue.Title, + FilePath: issue.FilePath, + FormatGroup: issue.FormatGroup, + LibraryTypeName: issue.LibraryTypeName, + IssueType: issue.IssueType, + IssueDescription: issue.IssueDescription, + Severity: issue.Severity, + CreatedAt: issue.CreatedAt.Time, + } + } + + return c.JSON(http.StatusOK, response) +} + +// GetProcessingIssueStats returns statistics about processing issues +func (h *ProcessingIssuesHandler) GetProcessingIssueStats(c *echo.Context) error { + libraryID, err := uuid.Parse(c.Param("libraryId")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid library ID"}) + } + + stats, err := h.db.GetProcessingIssueStats( + c.Request().Context(), + pgtype.UUID{Bytes: libraryID, Valid: true}, + ) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch stats"}) + } + + return c.JSON(http.StatusOK, ProcessingIssueStats{ + ErrorCount: stats.ErrorCount, + WarningCount: stats.WarningCount, + InfoCount: stats.InfoCount, + }) +} + +// ResolveProcessingIssue marks an issue as resolved +func (h *ProcessingIssuesHandler) ResolveProcessingIssue(c *echo.Context) error { + issueID, err := uuid.Parse(c.Param("issueId")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid issue ID"}) + } + + mediaItemID, err := uuid.Parse(c.Param("mediaItemId")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid media item ID"}) + } + + _, err = h.db.ResolveProcessingIssue( + c.Request().Context(), + issueID, + pgtype.UUID{Bytes: mediaItemID, Valid: true}, + ) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to resolve issue"}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "success": true, + "message": "Issue marked as resolved", + }) +} + +// DeleteProcessingIssue permanently deletes an issue record +func (h *ProcessingIssuesHandler) DeleteProcessingIssue(c *echo.Context) error { + issueID, err := uuid.Parse(c.Param("issueId")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid issue ID"}) + } + + _, err = h.db.DeleteProcessingIssue(c.Request().Context(), issueID) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to delete issue"}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "success": true, + "message": "Issue deleted", + }) +} diff --git a/templates/admin_processing_issues.templ b/templates/admin_processing_issues.templ new file mode 100644 index 0000000..a017738 --- /dev/null +++ b/templates/admin_processing_issues.templ @@ -0,0 +1,95 @@ + +package templates + +templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssueData, stats IssueStats) { + + + + + Processing Issues - Bookhoard + + + + @Header(user, "/admin/libraries/"+libraryID) +
+
+
+
+

Processing Issues

+

Items that couldn't be processed in this library

+
+ + ← Back to Library + +
+
+ if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 { + +
+ if stats.ErrorCount > 0 { +
+

Errors

+

{ stats.ErrorCount }

+
+ } + if stats.WarningCount > 0 { +
+

Warnings

+

{ stats.WarningCount }

+
+ } + if stats.InfoCount > 0 { +
+

Info

+

{ stats.InfoCount }

+
+ } +
+ } + if len(issues) == 0 { +
+

No processing issues found for this library.

+
+ } else { + +
+ for _, issue := range issues { +
+
+
+

{ issue.Title }

+

{ issue.IssueDescription }

+
+

Type: { issue.IssueType }

+

Format: { issue.FormatGroup }

+

File: { issue.FilePath }

+

Library: { issue.LibraryTypeName }

+
+
+
+ + { issue.Severity } + +
+
+
+ if issue.Severity == "warning" || issue.Severity == "info" { + + } +
+
+ } +
+ } +
+ + +}