package api import ( "context" "net/http" "strconv" "strings" "time" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/classes/crawl" "git.golem15.com/golem15/sm-seo-plugin/models" "gorm.io/gorm" ) // ListIssuesInput is the query of GET /_seo/api/v1/issues. type ListIssuesInput struct { Kind string `json:"kind,omitempty" query:"kind" jsonschema:"one kind only, e.g. h1_multiple, broken_link, missing_title, redirect_chain or sitemap_blocked"` } // IssueView is a stored issue in an answer: the Phase 32 keys (kind, url, // locale, details, tool) and the audit's id, severity, source, fix, // found_at, ignored and crawl_id. tool is the fixing MCP tool when fix is // tool; for fix code, details.what names the template element. type IssueView struct { ID uint `json:"id"` Kind string `json:"kind"` URL string `json:"url"` Locale string `json:"locale,omitempty"` Details map[string]any `json:"details,omitempty"` Tool string `json:"tool"` Severity string `json:"severity"` Source string `json:"source"` Fix string `json:"fix"` FoundAt *time.Time `json:"found_at"` Ignored bool `json:"ignored"` CrawlID *uint `json:"crawl_id"` } // issueViews turns stored issues into their answer form. func issueViews(db *gorm.DB, rows []models.Issue) ([]IssueView, error) { fps := make([]string, 0, len(rows)) for _, r := range rows { fps = append(fps, r.Fingerprint) } ignored, err := crawl.IgnoredSet(db, fps) if err != nil { return nil, err } out := make([]IssueView, 0, len(rows)) for _, r := range rows { out = append(out, IssueView{ ID: r.ID, Kind: r.Kind, URL: r.URL, Locale: r.Locale, Details: r.DetailsMap(), Tool: r.Tool, Severity: r.Severity, Source: r.Source, Fix: r.Fix, FoundAt: r.FoundAt, Ignored: ignored[r.Fingerprint], CrawlID: r.CrawlID, }) } return out, nil } // ListIssues serves GET /_seo/api/v1/issues (D-13, D-14): the audit's // current issues, errors first: the newest crawl's findings with the // CMS-data issues stored with it, or before any crawl the CMS-data issues // as they are now. Each issue names the tool that fixes it, or fix code. // counts holds the number per kind. func ListIssues(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { db, err := read(ctx, svc) if err != nil { return 0, nil, err } latest, err := crawl.Current(ctx, svc, db) if err != nil { return 0, nil, err } q := r.URL.Query() base := crawl.CurrentScope(db.Model(&models.Issue{}), latest) if id, err := strconv.ParseUint(strings.TrimSpace(q.Get("crawl")), 10, 64); err == nil && id > 0 { base = db.Model(&models.Issue{}).Where("golem15_seo_issues.crawl_id = ?", id) } var all []models.Issue if err := base.Order("rank ASC, kind ASC, url ASC, id ASC").Find(&all).Error; err != nil { return 0, nil, err } views, err := issueViews(db, all) if err != nil { return 0, nil, err } kind := strings.TrimSpace(q.Get("kind")) counts := map[string]int{} out := make([]IssueView, 0, len(views)) for _, v := range views { counts[v.Kind]++ if kind == "" || v.Kind == kind { out = append(out, v) } } return http.StatusOK, map[string]any{"data": out, "counts": counts, "total": len(out)}, nil }) }