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:
1 parent
19b9c7c9b8
commit
9b5defcb31
50 files changed
+1244
-681
No files matched your search
@@ -35,6 +35,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
@@ -536,7 +537,7 @@ func (s *MediaScanner) createAudioBookItem(ctx context.Context, libraryID pgtype
|
||||
// A2c §2.7: same title/subtitle-derived series fallback as the
|
||||
// single-file path — folder-of-tracks books carry the same naming.
|
||||
applyTitleDerivedSeries(metadata)
|
||||
return s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
created, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
||||
LibraryID: libraryID,
|
||||
Title: metadata.Title,
|
||||
Subtitle: pgtype.Text{String: metadata.Subtitle, Valid: metadata.Subtitle != ""},
|
||||
@@ -566,6 +567,20 @@ func (s *MediaScanner) createAudioBookItem(ctx context.Context, libraryID pgtype
|
||||
ImportedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
|
||||
AudioMetadata: marshalAudioMetadata(metadata.AudioMeta),
|
||||
})
|
||||
if err != nil {
|
||||
// Ledger (Phase 11): row-less import failure keyed by
|
||||
// (library, path) — the folder/zip book's row was never created.
|
||||
if logErr := s.LogRowlessProcessingIssue(ctx,
|
||||
uuid.UUID(libraryID.Bytes), relPath,
|
||||
"import_failed", fmt.Sprintf("File failed to import: %v", err), "error"); logErr != nil {
|
||||
fmt.Printf("Warning: failed to record import-failure issue for %s: %v\n", relPath, logErr)
|
||||
}
|
||||
return database.MediaItems{}, err
|
||||
}
|
||||
// Ledger self-heal (Phase 11): the book finally imports — close any
|
||||
// open row-less import-failure issues keyed to this (library, path).
|
||||
s.resolveIssuesForPath(ctx, libraryID, relPath)
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// setAudioFormatGroup writes the audiobook format facts after create.
|
||||
@@ -942,6 +957,10 @@ func (s *MediaScanner) processAudioFolderBook(ctx context.Context, rootDir strin
|
||||
if manifestChanged && !s.forceRescan {
|
||||
if applyErr := s.applyMultiFileSidecarEdit(ctx, existing, sentinel, metadata); applyErr != nil {
|
||||
fmt.Printf("Warning: %v\n", applyErr)
|
||||
} else {
|
||||
// Ledger self-heal (Phase 11): the book's metadata
|
||||
// re-applied successfully — close its open issues.
|
||||
s.resolveIssuesForItem(ctx, existing.ID)
|
||||
}
|
||||
// Fresh full manifest: the next scan compares against
|
||||
// TODAY's sidecars, not the stale pre-edit fingerprint.
|
||||
|
||||
Reference in new issue
Block a user