- 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
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
// Package controllers holds the SEO plugin's admin controllers.
|
|
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PermissionManage is the one SEO permission (D-15): the SEO tab, URL
|
|
// rules, redirects, the 404 log, robots.txt, settings and history.
|
|
const PermissionManage = "golem15.seo.manage"
|
|
|
|
// The admin controller IDs.
|
|
const (
|
|
SettingsControllerID = "golem15.seo.settings"
|
|
RulesControllerID = "golem15.seo.rules"
|
|
)
|
|
|
|
// AdminControllers is the plugin's admin controller list. app returns the
|
|
// booted application; it is asked on every call, so a controller never
|
|
// keeps a database handle of its own.
|
|
func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
|
return []pact.AdminController{
|
|
settingsAdminController{app: app},
|
|
redirectsAdminController{app: app},
|
|
notFoundAdminController{app: app},
|
|
rulesAdminController{app: app},
|
|
pagesAdminController{app: app},
|
|
issuesAdminController{app: app},
|
|
crawlsAdminController{app: app},
|
|
robotsAdminController{app: app},
|
|
historyAdminController{app: app},
|
|
transferAdminController{app: app},
|
|
}
|
|
}
|
|
|
|
// appOf returns the booted application, nil before boot.
|
|
func appOf(app func() *backpack.App) *backpack.App {
|
|
if app == nil {
|
|
return nil
|
|
}
|
|
return app()
|
|
}
|
|
|
|
// translator returns the application's translator, nil when none.
|
|
func translator(app func() *backpack.App) *phrasebook.Translator {
|
|
a := appOf(app)
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
tr, _ := a.Lookup[*phrasebook.Translator]()
|
|
return tr
|
|
}
|
|
|
|
// database returns the write transaction of ctx, else the app database.
|
|
func database(ctx context.Context, app func() *backpack.App) (*gorm.DB, error) {
|
|
if tx, ok := cabana.TxFromContext(ctx); ok && tx != nil {
|
|
return tx, nil
|
|
}
|
|
a := appOf(app)
|
|
if a == nil {
|
|
return nil, errors.New("golem15.seo: app is not booted")
|
|
}
|
|
db, ok := a.Lookup[*gorm.DB]()
|
|
if !ok || db == nil {
|
|
return nil, errors.New("golem15.seo: database is not configured")
|
|
}
|
|
return db.WithContext(ctx), nil
|
|
}
|
|
|
|
// Phrase translates key with params in the request locale; without a
|
|
// translator it returns the key.
|
|
func Phrase(ctx context.Context, tr *phrasebook.Translator, key string, params map[string]string) string {
|
|
if tr == nil {
|
|
return key
|
|
}
|
|
return tr.Get(ctx, key, params)
|
|
}
|
|
|
|
// problemsError turns field problems into a 422 with the messages
|
|
// translated in the request locale; nil when there are none.
|
|
func problemsError(ctx context.Context, tr *phrasebook.Translator, problems map[string][]models.Problem) error {
|
|
if len(problems) == 0 {
|
|
return nil
|
|
}
|
|
details := make(map[string]any, len(problems))
|
|
for field, list := range problems {
|
|
msgs := make([]string, 0, len(list))
|
|
for _, p := range list {
|
|
if len(p.Params) == 0 {
|
|
msgs = append(msgs, p.Key)
|
|
continue
|
|
}
|
|
msgs = append(msgs, Phrase(ctx, tr, p.Key, p.Params))
|
|
}
|
|
details[field] = msgs
|
|
}
|
|
return &cabana.ValidationError{Details: details}
|
|
}
|