Show scan warnings as structured cards instead of long sentences

Warnings are now stored and served as structured data (title, detail,
slave/master items, total) and rendered as cards with a Slave -> Master
table: the first five rows, with "Show all" for the rest.

- Renamed folders highlight the characters that differ; non-ASCII
  characters get their code point in a tooltip, so look-alikes such as
  "-" (U+002D) and "‐" (U+2010) can be told apart.
- Songs in a different folder are grouped by folder pair with a song
  count, most affected first, instead of two long examples.
- Folders that only differ by name are no longer also reported as
  songs in a different folder.
- Warnings saved as plain text by earlier scans still display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-23 21:49:22 -07:00
1 parent 1d31999793
commit 9e848cd765
13 files changed
+305 -92

No files matched your search

+6 -4
View File
@@ -340,14 +340,16 @@ func (e *Engine) scan(ctx context.Context, j *Job, pairID int64, trigger string)
}
// Layout warnings based on what was actually found.
if w := layout.warning(); w != "" {
warns = append(warns, w)
if w := layout.warning(); w != nil {
warns = append(warns, *w)
}
if ms.symlinks+ss.symlinks > 0 {
warns = append(warns, fmt.Sprintf("Ignored %d symlinks (playlink never follows or replaces symlinks).", ms.symlinks+ss.symlinks))
warns = append(warns, store.Warning{Title: fmt.Sprintf("Ignored %d symlinks", ms.symlinks+ss.symlinks),
Detail: "playlink never follows or replaces symlinks."})
}
if e.prober.Bin == "" {
warns = append(warns, "ffprobe was not found, so codec and quality details are unavailable for manual review.")
warns = append(warns, store.Warning{Title: "ffprobe was not found",
Detail: "Codec and quality details are unavailable for manual review."})
}
if err := e.st.FinishScan(ctx, pairID, time.Now().Unix(), "", warns); err != nil {
return err