package api import ( "context" "log" "vetting/internal/model" "vetting/internal/store" "vetting/internal/web/templates" ) // TileEnricher builds a fully-populated TileData for a host. It looks // up the latest run's spec-diff count and hold-key artifact path so the // tile can render the "n critical diffs" badge and the ssh invocation // without the template package needing DB access. // // Used by both the Dashboard handler (initial render) and the SSE tile- // refresh path (agent_handlers.Hold, orchestrator runner) so every // place that renders a tile shows the same data. type TileEnricher struct { Runs *store.Runs Artifacts *store.Artifacts SpecDiffs *store.SpecDiffs } // Build returns a TileData for (host, latest). Fails soft: DB errors // fall back to a tile without the extra fields rather than breaking // the whole dashboard. func (e *TileEnricher) Build(ctx context.Context, host model.Host, latest *model.Run) templates.TileData { t := templates.TileData{Host: host, Latest: latest} if latest == nil { return t } if e.SpecDiffs != nil { if diffs, err := e.SpecDiffs.ListForRun(ctx, latest.ID); err == nil { for _, d := range diffs { if d.Severity == "critical" && !d.Ignored { t.SpecDiffCritical++ } } } else { log.Printf("tile: list spec_diffs run %d: %v", latest.ID, err) } } if e.Artifacts != nil { if arts, err := e.Artifacts.ListForRun(ctx, latest.ID); err == nil { for _, a := range arts { if a.Kind == "hold_key" { t.HoldKeyPath = a.Path } } } else { log.Printf("tile: list artifacts run %d: %v", latest.ID, err) } } return t } // BuildByHost looks up the latest run itself — convenient for SSE tile // publishers that only know the host ID. func (e *TileEnricher) BuildByHost(ctx context.Context, host model.Host) templates.TileData { var latest *model.Run if e.Runs != nil { if r, err := e.Runs.LatestForHost(ctx, host.ID); err == nil { latest = r } } return e.Build(ctx, host, latest) }