feat(issues): the honest processing-issues ledger — row-less import failures, self-healing resolution, family lens (faceted plan Phase 11)

processing_issues only recorded cross-library move rejections; files
that failed to import vanished into an error counter and stdout. The
ledger now records every failure class and heals itself:

Schema (in place, pre-release boundary):
- media_item_id NULLABLE + file_path TEXT (deliberately unbounded —
  an over-long path is itself a failure mode; a VARCHAR(500) ledger
  column died on the exact 22001 it existed to report — caught by the
  new test). Row-less failures key on (library, file_path); item-backed
  on (item, type). Widened dedup = COALESCE-expression unique index;
  the old table UNIQUE stays (NULLs pairwise-distinct for row-less).
- Conflict-target upsert: repeat failures UPDATE the same row and
  REOPEN resolved ones — never duplicates.

Wirings:
- CreateMediaItem failure (single-file + multifile createAudioBookItem)
  → row-less import_failed (error).
- Extraction hard-fail with fallback import → item-backed
  metadata_extraction_failed (warning).
- Change-path re-extraction failure (the ev/sidecar guard) → item-backed
  metadata_extraction_failed; LogProcessingIssue keeps its signature, a
  row-less helper joins it.

Auto-resolution:
- Successful import → ResolveOpenProcessingIssuesForPath (library,
  file_path).
- Successful (re)extraction → ResolveOpenProcessingIssuesForItem
  (single-file change path, multifile editorial apply).
- Unchanged-skip scans do NOT resolve: extraction never re-ran, so the
  degraded metadata is still degraded — the page heals exactly when the
  book does.

Family lens on ALL issues surfaces (list, stats, resolve, delete, SSR
page): explicit out-of-set library id → 403, never silent. This
tightens one pre-lens behavior — a nonexistent library id returned
200-empty, now 403 (pinned, changelog entry in docs/contract_pins.md).
List is LEFT-JOINed so row-less rows appear (media_item_id "",
title falls back to the path).

Tests: full lifecycle state machine through real scans (fail → appears,
repeat → no dups, success-at-path → resolved, LEFT-JOIN list shape);
family lens 403s with a fresh non-shared admin; all 7 pre-existing
issue suites green (two expectations widened to the lens, changelogged);
adjacent suites (CrossLibraryMove, Audiobook, Calibre,
ExtractorVersion, SchemaIdempotency) green; unit suites green.
This commit is contained in:
John O'Keefe committed 2026-10-20 19:38:02 -04:00
1 parent 19b9c7c9b8
commit 9b5defcb31
50 files changed
+1244 -681

No files matched your search

+21 -3
View File
@@ -1223,6 +1223,15 @@ func registerFrontendRoutes(cfg *Config) {
if err != nil {
return renderErrorPage(c, "Invalid library ID", "invalid_id")
}
// Family lens: issues leak media metadata (titles, file paths) —
// a library outside the requester's visible set renders nothing.
visibleSet, _, visErr := visibleLibraryIDsFor(c, cfg)
if visErr != nil {
return renderErrorPage(c, "Error checking library access", "visibility_error")
}
if !visibleSet[libraryID] {
return renderErrorPage(c, "Library not accessible", "library_not_accessible")
}
// Fetch stats
stats, err := cfg.Queries.GetProcessingIssueStats(
c.Request().Context(),
@@ -1239,14 +1248,23 @@ func registerFrontendRoutes(cfg *Config) {
if err != nil {
return renderErrorPage(c, "Error loading issues", "issues_load_error")
}
// Convert to template data
// Convert to template data (row-less import failures have no
// media item: empty id, and the title falls back to the path).
issues := make([]templates.ProcessingIssueData, len(dbIssues))
for i, issue := range dbIssues {
issueUUID, _ := uuid.FromBytes(issue.ID.Bytes[0:16])
itemID := ""
if issue.MediaItemID.Valid {
itemID = uuid.UUID(issue.MediaItemID.Bytes).String()
}
title := issue.Title
if title == "" {
title = issue.FilePath
}
issues[i] = templates.ProcessingIssueData{
ID: issueUUID.String(),
MediaItemID: uuid.UUID(issue.MediaItemID.Bytes).String(),
Title: issue.Title,
MediaItemID: itemID,
Title: title,
FilePath: issue.FilePath,
FormatGroup: issue.FormatGroup,
LibraryTypeName: issue.LibraryTypeName,