- golem15_seo_crawls, crawl pages, links, issues and ignores (P33-37) - classes/crawl: fetchguard client on the site host and its www twin, sitemap index and robots.txt seeds, two workers with a delay, limits - h1_missing and h1_multiple with the P33-38 fix route - POST /_seo/api/v1/crawls queues the golem15.seo.crawl job; GET /issues answers the stored issues with id, severity, source, fix, found_at, ignored and crawl_id
157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Where a stored issue was found (UI-SPEC C23).
|
|
const (
|
|
SourceCrawl = "crawl"
|
|
SourceCMS = "cms"
|
|
)
|
|
|
|
// How an issue is fixed (P33-38): through an MCP tool that changes CMS
|
|
// data, or in the site's template code.
|
|
const (
|
|
FixTool = "tool"
|
|
FixCode = "code"
|
|
)
|
|
|
|
// The severities of an issue and their sort rank (errors first).
|
|
const (
|
|
SeverityError = "error"
|
|
SeverityWarning = "warning"
|
|
SeverityNotice = "notice"
|
|
)
|
|
|
|
// SeverityRank orders the severities: error 1, warning 2, notice 3.
|
|
func SeverityRank(severity string) int {
|
|
switch severity {
|
|
case SeverityError:
|
|
return 1
|
|
case SeverityWarning:
|
|
return 2
|
|
}
|
|
return 3
|
|
}
|
|
|
|
// Issue is one stored finding of the site audit on golem15_seo_issues
|
|
// (P33-37): the crawl it belongs to (nil for CMS-data issues stored before
|
|
// any crawl), where it was found, its kind and severity, the address and
|
|
// locale, the kind's details, the fixing MCP tool or fix code, and the
|
|
// fingerprint an ignore is keyed by.
|
|
type Issue struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
CrawlID *uint `gorm:"column:crawl_id"`
|
|
Source string `gorm:"column:source"`
|
|
Kind string `gorm:"column:kind"`
|
|
Severity string `gorm:"column:severity"`
|
|
Rank int `gorm:"column:rank"`
|
|
URL string `gorm:"column:url"`
|
|
Locale string `gorm:"column:locale"`
|
|
Details lagoon.Jsonable[map[string]any] `gorm:"column:details"`
|
|
Tool string `gorm:"column:tool"`
|
|
Fix string `gorm:"column:fix"`
|
|
Fingerprint string `gorm:"column:fingerprint"`
|
|
FoundAt *time.Time `gorm:"column:found_at"`
|
|
// The list's values, filled by the Site audit controller's
|
|
// ListExtendRecords and never saved.
|
|
Summary string `gorm:"-:all;column:summary"`
|
|
FixCell string `gorm:"-:all;column:fix_cell"`
|
|
KindCell string `gorm:"-:all;column:kind_cell"`
|
|
}
|
|
|
|
func (Issue) TableName() string { return "golem15_seo_issues" }
|
|
|
|
// MorphName names an issue in the admin.
|
|
func (Issue) MorphName() string { return `Golem15\SEO\Models\Issue` }
|
|
|
|
// Fillable is empty: issues are written by the audit only.
|
|
func (Issue) Fillable() []string { return []string{} }
|
|
|
|
// Rules is empty: nothing is edited.
|
|
func (Issue) Rules() map[string]string { return map[string]string{} }
|
|
|
|
// DetailsMap returns the issue's details, never nil.
|
|
func (i Issue) DetailsMap() map[string]any {
|
|
if d := i.Details.Get(); d != nil {
|
|
return d
|
|
}
|
|
return map[string]any{}
|
|
}
|
|
|
|
// IssueIgnore is one ignored fingerprint on golem15_seo_issue_ignores: an
|
|
// issue with this fingerprint stays hidden in later crawls until it is no
|
|
// longer ignored (UI-SPEC C23).
|
|
type IssueIgnore struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Fingerprint string `gorm:"column:fingerprint"`
|
|
AdminID *uint `gorm:"column:admin_id"`
|
|
IgnoredAt *time.Time `gorm:"column:ignored_at"`
|
|
}
|
|
|
|
func (IssueIgnore) TableName() string { return "golem15_seo_issue_ignores" }
|
|
|
|
// The Site audit list's filter scopes and the status and fix values.
|
|
const (
|
|
FilterIssueSeverity = "FilterIssueSeverity"
|
|
FilterIssueKind = "FilterIssueKind"
|
|
FilterIssueSource = "FilterIssueSource"
|
|
FilterIssueFix = "FilterIssueFix"
|
|
FilterIssueStatus = "FilterIssueStatus"
|
|
IssueOpen = "open"
|
|
IssueIgnored = "ignored"
|
|
)
|
|
|
|
// IgnoredSQL is true for an issue whose fingerprint is ignored.
|
|
const IgnoredSQL = `EXISTS (SELECT 1 FROM golem15_seo_issue_ignores AS ig WHERE ig.fingerprint = golem15_seo_issues.fingerprint)`
|
|
|
|
// FilterScopes lists the Site audit list's filter scopes.
|
|
func (Issue) FilterScopes() []string {
|
|
return []string{FilterIssueSeverity, FilterIssueKind, FilterIssueSource, FilterIssueFix, FilterIssueStatus}
|
|
}
|
|
|
|
// FilterScope narrows the Site audit list; an unknown value matches
|
|
// nothing.
|
|
func (Issue) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
if db == nil {
|
|
return db
|
|
}
|
|
s, _ := value.(string)
|
|
switch name {
|
|
case FilterIssueSeverity:
|
|
if s == SeverityError || s == SeverityWarning || s == SeverityNotice {
|
|
return db.Where("golem15_seo_issues.severity = ?", s)
|
|
}
|
|
case FilterIssueKind:
|
|
if s != "" {
|
|
return db.Where("golem15_seo_issues.kind = ?", s)
|
|
}
|
|
case FilterIssueSource:
|
|
if s == SourceCrawl || s == SourceCMS {
|
|
return db.Where("golem15_seo_issues.source = ?", s)
|
|
}
|
|
case FilterIssueFix:
|
|
if s == FixTool || s == FixCode {
|
|
return db.Where("golem15_seo_issues.fix = ?", s)
|
|
}
|
|
case FilterIssueStatus:
|
|
switch s {
|
|
case IssueOpen:
|
|
return db.Where("NOT " + IgnoredSQL)
|
|
case IssueIgnored:
|
|
return db.Where(IgnoredSQL)
|
|
}
|
|
default:
|
|
return db
|
|
}
|
|
return db.Where("1 = 0")
|
|
}
|
|
|
|
func init() {
|
|
Register(Issue{}, IssueIgnore{})
|
|
}
|