feat: management API and an MCP tool per SEO admin operation
- /_seo/api/v1 routes for the effective SEO of a URL with sources and owner, URL rules, redirects (loops refused with redirect_loop), the 404 log actions, robots.txt and its tester, settings and history - the CMS-data issue report: missing and duplicate titles and descriptions, rules over records, redirect chains, canonicals to noindex or redirected addresses, noindex addresses robots.txt blocks - 24 MCP tools on those routes (backend guard, golem15.seo.manage) - AdminOperations maps every admin operation to its tool; TestEveryAdminOperationHasTool and TestMCPReadToolsAreGET guard drift - robots.txt writes go through classes.SaveRobots from the admin too
This commit is contained in:
95
controllers/api/history.go
Normal file
95
controllers/api/history.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
)
|
||||
|
||||
// ListHistoryInput is the query of GET /_seo/api/v1/history.
|
||||
type ListHistoryInput struct {
|
||||
Page int `json:"page,omitempty" query:"page" jsonschema:"the page number, from 1"`
|
||||
PerPage int `json:"per_page,omitempty" query:"per_page" jsonschema:"rows per page, at most 200 (50 by default)"`
|
||||
Search string `json:"search,omitempty" query:"search" jsonschema:"text the description contains"`
|
||||
Via string `json:"via,omitempty" query:"via" jsonschema:"admin, mcp, import, auto or undo"`
|
||||
Kind string `json:"kind,omitempty" query:"kind" jsonschema:"entry, rule, redirect, robots, settings or notfound"`
|
||||
Batch string `json:"batch,omitempty" query:"batch" jsonschema:"a batch id: the changes of one batch or import"`
|
||||
}
|
||||
|
||||
// HistoryIDInput names one history entry.
|
||||
type HistoryIDInput struct {
|
||||
ID uint `json:"id" path:"id" jsonschema:"the history entry id"`
|
||||
}
|
||||
|
||||
// RevisionJSON is a history entry in an answer.
|
||||
func RevisionJSON(r models.Revision) map[string]any {
|
||||
batch := ""
|
||||
if r.BatchID != nil {
|
||||
batch = *r.BatchID
|
||||
}
|
||||
return map[string]any{
|
||||
"id": r.ID, "created_at": r.CreatedAt, "author": r.AuthorLabel, "author_id": r.AuthorID,
|
||||
"via": r.Via, "kind": r.SubjectKind, "subject": r.SubjectID, "what": r.What,
|
||||
"before": r.Before, "after": r.After, "batch_id": batch, "undone_at": r.UndoneAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ListHistory serves GET /_seo/api/v1/history: newest first, filtered by
|
||||
// writer, subject kind, batch and description text (D-13).
|
||||
func ListHistory(service func() *classes.Service) http.HandlerFunc {
|
||||
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
||||
db, err := read(ctx, svc)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
in := listParams(r)
|
||||
q := db.Model(&models.Revision{}).Order("id DESC")
|
||||
if in.Search != "" {
|
||||
q = q.Where("lower(what) LIKE ?", like(in.Search))
|
||||
}
|
||||
query := r.URL.Query()
|
||||
if v := query.Get("via"); v != "" {
|
||||
q = q.Where("via = ?", v)
|
||||
}
|
||||
if v := query.Get("kind"); v != "" {
|
||||
q = q.Where("subject_kind = ?", v)
|
||||
}
|
||||
if v := query.Get("batch"); v != "" {
|
||||
q = q.Where("batch_id = ?", v)
|
||||
}
|
||||
var rows []models.Revision
|
||||
total, err := paginate(q, in, &rows)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
out := make([]map[string]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, RevisionJSON(row))
|
||||
}
|
||||
return http.StatusOK, listBody(out, in, total), nil
|
||||
})
|
||||
}
|
||||
|
||||
// ShowHistory serves GET /_seo/api/v1/history/{id}.
|
||||
func ShowHistory(service func() *classes.Service) http.HandlerFunc {
|
||||
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
||||
id, err := pathID(r, "id")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
db, err := read(ctx, svc)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var rows []models.Revision
|
||||
if err := db.Where("id = ?", id).Limit(1).Find(&rows).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return 0, nil, classes.ErrNoRecord
|
||||
}
|
||||
return http.StatusOK, map[string]any{"data": RevisionJSON(rows[0])}, nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user