feat: Site audit and Crawls screens with their MCP tools

- golem15.seo.issues (Site audit): current issues errors first, labels,
  summary and fix columns, filters, P7 issue_details, Check this address
  again, Ignore, Stop ignoring, bulk Ignore, ignored rows badged
- golem15.seo.crawls (Crawls): newest first, P6 crawl_summary with one
  status callout, staging and rendered-comparison lines, stats strip and
  per-kind table; Cancel; a running crawl refuses delete
- Start crawl toolbar action on both, refusing already_running and
  no_site_url; navigation Site audit (gauge) and Crawls (globe)
- routes and MCP tools start/cancel/list/get/delete crawls, list_issues
  with filters (open by default), recheck_url, ignore and unignore
- AdminOperations maps both controllers; toolbar actions get their own
  operation kind
- TestCrawlAdmin, TestCrawlTools, TestIssueKindLabels
This commit is contained in:
Jakub Zych
2026-11-04 15:34:22 +01:00
parent edc03cae4b
commit c4f0b4f94c
32 changed files with 2778 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"git.golem15.com/golem15/sm-seo-plugin/classes/schema"
@@ -427,3 +428,110 @@ func (s *Service) primaryGoneCallout(ctx context.Context, db *gorm.DB, key, id s
Title: s.PhraseCtx(ctx, "golem15.seo::lang.status.primary_gone", map[string]string{"name": name}),
}, true
}
// IssueParams turns an issue's details into the :name parameters of its
// issues.sum_<kind> phrase: scalars as text, lists by their length or
// joined (fields, also), with the aliases the Phase 32 kinds need (count
// from also or hops, url from canonical).
func IssueParams(details map[string]any) map[string]string {
out := map[string]string{}
for k, v := range details {
switch x := v.(type) {
case string:
out[k] = x
case float64:
out[k] = strconv.FormatFloat(x, 'f', -1, 64)
case int:
out[k] = strconv.Itoa(x)
case bool:
out[k] = strconv.FormatBool(x)
case []any:
parts := make([]string, 0, len(x))
for _, e := range x {
if s, ok := e.(string); ok {
parts = append(parts, s)
}
}
out[k] = strings.Join(parts, ", ")
case []string:
out[k] = strings.Join(x, ", ")
}
}
if _, ok := out["count"]; !ok {
if also, ok := details["also"].([]any); ok {
out["count"] = strconv.Itoa(len(also))
} else if hops, ok := out["hops"]; ok {
out["count"] = hops
}
}
if _, ok := out["url"]; !ok {
if c, ok := out["canonical"]; ok {
out["url"] = c
}
}
return out
}
// IssueSummary is the one-line Details cell of an issue (issues.sum_<kind>).
func (s *Service) IssueSummary(ctx context.Context, kind string, details map[string]any) string {
return s.PhraseCtx(ctx, "golem15.seo::lang.issues.sum_"+kind, IssueParams(details))
}
// IssueRow is one row of P7's "What was found" table.
type IssueRow struct {
Label string
Value string
Mono bool
}
// IssueLink is a page named in P7 (a linking page or a suggestion): its
// address and, when the page has an admin form, the admin path.
type IssueLink struct {
URL string
Admin string
Phrase string
}
// IssueDetailsView is the P7 issue_details view model (UI-SPEC C23).
type IssueDetailsView struct {
Rows []IssueRow
LinkedFrom []IssueLink
More string
Orphan bool
Suggestions []IssueLink
FixText string
PageURL string
RichResults bool
RichURL string
IgnoredNotes string
}
// CrawlStat is one item of P6's stats strip.
type CrawlStat struct {
Label string
Value string
}
// CrawlKindRow is one row of P6's per-kind table.
type CrawlKindRow struct {
Kind string
Label string
Severity string
Count int
Link string
}
// CrawlSummaryView is the P6 crawl_summary view model (UI-SPEC C24): one
// status callout (Tone "", warning or danger), the staging and rendered
// comparison lines, the stats strip and the per-kind table.
type CrawlSummaryView struct {
Tone string
Title string
Text string
Staging string
Render string
Stats []CrawlStat
Kinds []CrawlKindRow
NoIssues bool
HasCallout bool
}