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 -5
View File
@@ -255,7 +255,7 @@ func (e *Engine) open(p store.Pair) (*libs, error) {
// NormalizePair fills defaults and validates a pair. It returns non-fatal
// warnings (such as layout mismatches) for the UI.
func (e *Engine) NormalizePair(p *store.Pair) ([]string, error) {
func (e *Engine) NormalizePair(p *store.Pair) ([]store.Warning, error) {
p.Name = strings.TrimSpace(p.Name)
if p.Name == "" {
return nil, errors.New("name is required")
@@ -301,7 +301,7 @@ func (e *Engine) NormalizePair(p *store.Pair) ([]string, error) {
// layoutWarnings is the quick check run when a pair is saved and at the
// start of each scan. Content that exists in only one library is normal and
// never warned about; only folders that are the same but named differently are.
func layoutWarnings(l *libs) []string {
func layoutWarnings(l *libs) []store.Warning {
empty := true
if ents, _ := os.ReadDir(l.slave.Path); len(ents) > 0 {
for _, en := range ents {
@@ -312,10 +312,11 @@ func layoutWarnings(l *libs) []string {
}
}
if empty {
return []string{"The slave library is empty. Use Build to pick songs from the master library to hardlink into it."}
return []store.Warning{{Title: "The slave library is empty",
Detail: "Use Build to pick songs from the master library to hardlink into it."}}
}
if w := renamedWarning(renamedFolders(l)); w != "" {
return []string{w}
if w := renamedWarning(renamedFolders(l)); w != nil {
return []store.Warning{*w}
}
return nil
}