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{}) }