refactor: share the explain kind list and time format, trim the DTO

The explainable-kind list and the report's time format each existed twice,
kept in sync by a comment pointing at the other copy. Both now live in
core/artwork beside their siblings.

FormatAgents returned a CLI-worded sentence, so the web UI rendered starred
agents with no legend at all. It now reports a bool and each caller words its
own note.

Drop kind, id, chainOrigin and the numeric priority from the explain
response: all four were serialized and never read, and chainOrigin is
derivable from stored.attemptedAt on an endpoint that never walks.

ExpandInfoDialog takes an optional resource instead of a node-or-map content
prop. A page mounts one dialog per resource, which removes the silent
blank-dialog failure mode when a map was missing a key.

Outcome chips read the MUI palette rather than hardcoded hex, so they follow
the dark theme.
This commit is contained in:
Deluan 2026-09-04 07:29:54 -04:00
commit 8ba8e7f2d7
13 changed files with 142 additions and 136 deletions

View file

@ -13,13 +13,6 @@ import (
"github.com/navidrome/navidrome/model"
)
// explainableKinds is every kind the endpoint accepts, matching the CLI's explainKinds: a kind
// with no chain to walk still has stored state and config to report.
var explainableKinds = []model.Kind{
model.KindArtistArtwork, model.KindAlbumArtwork, model.KindDiscArtwork,
model.KindMediaFileArtwork, model.KindPlaylistArtwork, model.KindRadioArtwork,
}
type traceStepDTO struct {
Candidate string `json:"candidate"`
Outcome string `json:"outcome"`
@ -33,7 +26,6 @@ type storedDTO struct {
}
type queuedDTO struct {
Priority int `json:"priority"`
PriorityName string `json:"priorityName"`
Attempts int `json:"attempts"`
RetryAt string `json:"retryAt,omitempty"`
@ -45,11 +37,8 @@ type configDTO struct {
}
type explainDTO struct {
Kind string `json:"kind"`
ID string `json:"id"`
Name string `json:"name"`
Result string `json:"result"`
ChainOrigin string `json:"chainOrigin"`
Steps []traceStepDTO `json:"steps"`
Stored *storedDTO `json:"stored,omitempty"`
Queued *queuedDTO `json:"queued,omitempty"`
@ -57,6 +46,7 @@ type explainDTO struct {
GaveUpAfter []traceStepDTO `json:"gaveUpAfter,omitempty"`
Config *configDTO `json:"config,omitempty"`
Agents string `json:"agents,omitempty"`
AgentsIncomplete bool `json:"agentsIncomplete,omitempty"`
}
func (api *Router) addArtworkExplainRoute(r chi.Router) {
@ -67,7 +57,7 @@ func (api *Router) explainArtwork() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
kind, ok := model.ParseKind(r.URL.Query().Get("kind"))
if !ok || !slices.Contains(explainableKinds, kind) {
if !ok || !slices.Contains(artwork.ExplainKinds, kind) {
http.Error(w, "invalid artwork kind", http.StatusBadRequest)
return
}
@ -110,13 +100,11 @@ func toStepDTOs(steps []artwork.TraceStep) []traceStepDTO {
func toExplainDTO(rep artwork.ExplainReport) explainDTO {
dto := explainDTO{
Kind: rep.Kind.Prefix(),
ID: rep.ID,
Name: rep.Name,
Result: rep.Result(),
ChainOrigin: rep.ChainOrigin(),
Steps: toStepDTOs(rep.Steps),
Agents: rep.Agents,
Name: rep.Name,
Result: rep.Result(),
Steps: toStepDTOs(rep.Steps),
Agents: rep.Agents,
AgentsIncomplete: rep.AgentsIncomplete,
}
if rep.Stored != nil {
dto.Stored = &storedDTO{
@ -127,7 +115,6 @@ func toExplainDTO(rep artwork.ExplainReport) explainDTO {
}
if rep.Queued != nil {
dto.Queued = &queuedDTO{
Priority: rep.Queued.Priority,
PriorityName: artwork.PriorityName(rep.Queued.Priority),
Attempts: rep.Queued.Attempts,
RetryAt: rfc3339(rep.Queued.RetryAt),

View file

@ -90,7 +90,6 @@ var _ = Describe("GET /artwork/explain", func() {
Expect(json.Unmarshal(w.Body.Bytes(), &got)).To(Succeed())
Expect(got["name"]).To(Equal("Radiohead"))
Expect(got["result"]).To(Equal("resolved from external:deezer"))
Expect(got["chainOrigin"]).To(ContainSubstring("recorded"))
Expect(got["config"]).To(HaveKeyWithValue("setting", "ArtistArtPriority"))
Expect(got["stored"]).To(Equal(map[string]any{
@ -127,7 +126,6 @@ var _ = Describe("GET /artwork/explain", func() {
var got map[string]any
Expect(json.Unmarshal(w.Body.Bytes(), &got)).To(Succeed())
Expect(got["queued"]).To(Equal(map[string]any{
"priority": float64(model.ArtworkPriorityScan),
"priorityName": "scan",
"attempts": float64(1),
"retryAt": retryAt.Format(time.RFC3339),
@ -149,6 +147,6 @@ var _ = Describe("GET /artwork/explain", func() {
Expect(got).ToNot(HaveKey("queued"))
Expect(got).ToNot(HaveKey("lastAttemptFailed"))
Expect(got).ToNot(HaveKey("gaveUpAfter"))
Expect(got["chainOrigin"]).To(Equal("not recorded"))
Expect(got).ToNot(HaveKey("chainOrigin"))
})
})