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)
+
+
+ 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" {
+
+ }
+
+
+ }
+
+ }
+
+
+
+}