feat: SEO change history

This commit is contained in:
2026-10-27 10:45:30 +01:00
parent 9f1082d62d
commit e8a6b9fc77
21 changed files with 1070 additions and 26 deletions

91
models/revision.go Normal file
View File

@@ -0,0 +1,91 @@
package models
import (
"time"
"gorm.io/gorm"
)
// The writers of a history entry (D-13).
const (
ViaAdmin = "admin"
ViaMCP = "mcp"
ViaImport = "import"
ViaAuto = "auto"
ViaUndo = "undo"
)
// The subjects of a history entry.
const (
SubjectEntry = "entry"
SubjectRule = "rule"
SubjectRedirect = "redirect"
SubjectRobots = "robots"
SubjectSettings = "settings"
SubjectNotFound = "notfound"
)
// Revision is one recorded SEO change on golem15_seo_revisions (D-13): who
// made it (AuthorID and AuthorLabel, the label kept when the administrator
// is gone), through what (Via), on which object (SubjectKind and
// SubjectID: an entry as "<model type>|<record id>|<locale>", a rule by
// id, settings as "1" or "type:<key>"), a readable description (What) and
// the changed fields only, before and after. A created object has no
// Before and a deleted one no After. BatchID groups the changes of one
// batch; UndoneAt marks an undone change.
type Revision struct {
ID uint `gorm:"column:id;primaryKey"`
BatchID *string `gorm:"column:batch_id"`
AuthorID *uint `gorm:"column:author_id"`
AuthorLabel string `gorm:"column:author_label"`
Via string `gorm:"column:via"`
SubjectKind string `gorm:"column:subject_kind"`
SubjectID string `gorm:"column:subject_id"`
What string `gorm:"column:what"`
Before JSONMap `gorm:"column:before"`
After JSONMap `gorm:"column:after"`
UndoneAt *time.Time `gorm:"column:undone_at"`
UndoneByID *uint `gorm:"column:undone_by_id"`
CreatedAt time.Time `gorm:"column:created_at"`
// Change is the list's summary of the changed fields and ViaLabel the
// via column's label, filled by the history controller.
Change string `gorm:"-:all;column:change"`
ViaLabel string `gorm:"-:all;column:via_label"`
Batch string `gorm:"-:all;column:batch"`
}
func (Revision) TableName() string { return "golem15_seo_revisions" }
// Fillable is empty: history is written by the service only.
func (Revision) Fillable() []string { return []string{} }
// Rules is empty: history is never edited.
func (Revision) Rules() map[string]string { return map[string]string{} }
// FilterVia is the history list's via filter scope.
const FilterVia = "FilterVia"
// Vias lists the writers in display order.
var Vias = []string{ViaAdmin, ViaMCP, ViaImport, ViaAuto, ViaUndo}
func init() {
Register(Revision{})
}
// FilterScopes lists the history list's filter scopes.
func (Revision) FilterScopes() []string { return []string{FilterVia} }
// FilterScope narrows the history list to one writer; an unknown value
// matches nothing.
func (Revision) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
if db == nil || name != FilterVia {
return db
}
s, _ := value.(string)
for _, v := range Vias {
if v == s {
return db.Where("golem15_seo_revisions.via = ?", s)
}
}
return db.Where("1 = 0")
}

View File

@@ -0,0 +1,28 @@
# History list columns (UI-SPEC C12). via_label, change and batch are
# filled by the controller's ListExtendRecords.
columns:
created_at:
label: golem15.seo::lang.history.when
type: datetime
sortable: true
author_label:
label: golem15.seo::lang.history.who
type: text
searchable: true
via_label:
label: golem15.seo::lang.history.via
type: text
sortable: false
what:
label: golem15.seo::lang.history.what
type: text
searchable: true
change:
label: golem15.seo::lang.history.change
type: text
sortable: false
batch:
label: golem15.seo::lang.history.batch
type: text
sortable: false
invisible: true

View File

@@ -0,0 +1,28 @@
# History preview (UI-SPEC C12): read-only fields, then the P5 diff.
fields:
created_at:
label: golem15.seo::lang.history.when
type: text
span: left
context: preview
author_label:
label: golem15.seo::lang.history.who
type: text
span: right
context: preview
via:
label: golem15.seo::lang.history.via
type: text
span: left
context: preview
what:
label: golem15.seo::lang.history.what
type: text
span: right
context: preview
_history_diff:
label: golem15.seo::lang.history.change
type: partial
path: history_diff
span: full
context: preview