Files
llmgw/internal/web/admin_guard.go
T
Ilker ManapandClaude Opus 5.5 11231d8c2b The portal in English and Turkish: a catalogue per language, a switcher at the foot of every page, landing.language, Turkish numbers and dates
Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:12:53 +02:00

144 lines
4.5 KiB
Go

package web
import (
"context"
"net/http"
"slices"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/guard"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
// GuardAdminSource is what the guardrails page reads.
type GuardAdminSource interface {
GuardEvents(ctx context.Context, since time.Time, limit int) ([]store.GuardEvent, error)
GuardCounts(ctx context.Context, since time.Time) ([]store.GuardCount, error)
GuardAttachments(ctx context.Context) ([]guard.Attachment, error)
AllProjects(ctx context.Context) ([]store.ProjectPath, error)
}
type guardPageData struct {
Nav Nav
Version string
Window string
Windows []string
// Policies are the library, Default those applied to every request.
Policies []string
Default []string
Attachments []guardAttachmentRow
Counts []guardCountRow
Events []guardEventRow
Error string
}
type guardAttachmentRow struct{ Scope, Who, Policy string }
type guardCountRow struct {
Where, Action, Policy, Category string
Count int64
}
type guardEventRow struct {
At, RequestID, Where, Model, Action, Policy, Category string
Status int
Ms int64
}
var guardWindows = map[string]time.Duration{"24h": 24 * time.Hour, "7d": 7 * 24 * time.Hour, "30d": 30 * 24 * time.Hour}
// RegisterAdminGuard mounts /admin/guard: what the guardrails did, where, by
// which policy — names and counts, never text.
func (p *AccountPages) RegisterAdminGuard(mux *http.ServeMux, s GuardAdminSource, library guard.Config) {
p.hasGuard = true
mux.HandleFunc("GET /admin/guard", p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) {
ctx, l := r.Context(), langOf(r)
d := guardPageData{Nav: p.navFor(r, admin, "guard"), Version: p.version, Window: r.URL.Query().Get("window"),
Windows: []string{"24h", "7d", "30d"}, Default: library.Default}
since, ok := guardWindows[d.Window]
if !ok {
d.Window, since = "7d", guardWindows["7d"]
}
for name := range library.Policies {
d.Policies = append(d.Policies, name)
}
slices.Sort(d.Policies)
where := map[string]string{"": l.T("guard.personal")}
if projects, err := s.AllProjects(ctx); err == nil {
for _, pp := range projects {
where[pp.ID] = pp.Path
}
}
name := func(id string) string {
if n, ok := where[id]; ok {
return n
}
return id
}
if att, err := s.GuardAttachments(ctx); err == nil {
for _, a := range att {
who := a.ScopeID
if a.Scope == "project" {
who = name(a.ScopeID)
}
d.Attachments = append(d.Attachments, guardAttachmentRow{Scope: guardScopeWord(l, a.Scope), Who: who, Policy: a.Policy})
}
}
counts, err := s.GuardCounts(ctx, time.Now().Add(-since))
if err != nil {
d.Error = l.Err(err)
}
for _, c := range counts {
d.Counts = append(d.Counts, guardCountRow{Where: name(c.ProjectID), Action: guardActionWord(l, c.Action),
Policy: c.Policy, Category: c.Category, Count: c.Count})
}
events, err := s.GuardEvents(ctx, time.Now().Add(-since), 100)
if err != nil {
d.Error = l.Err(err)
}
for _, e := range events {
// The day as the reader writes one; the seconds stay, because
// events a second apart are what this table tells apart.
at := e.At.UTC()
d.Events = append(d.Events, guardEventRow{At: l.Date(at) + " " + at.Format("15:04:05"),
RequestID: e.RequestID, Where: name(e.ProjectID), Model: e.Model, Action: guardActionWord(l, e.Action),
Policy: e.Policy, Category: e.Category, Status: e.Status, Ms: e.GuardMs})
}
noStore(w)
render(w, r, "admin_guard", d)
}))
}
// guardScopeWord names the kind of scope a policy is attached to in the
// reader's language. A kind this page does not know is shown as stored.
func guardScopeWord(l *Lang, scope string) string {
switch scope {
case "org":
return l.T("guard.scope.org")
case "team":
return l.T("guard.scope.team")
case "project":
return l.T("guard.scope.project")
case "user":
return l.T("guard.scope.user")
case "key":
return l.T("guard.scope.key")
}
return scope
}
// guardActionWord is what a policy did, in the reader's language. The stored
// value stays the English word the events are recorded and queried by.
func guardActionWord(l *Lang, action string) string {
switch action {
case "flagged":
return l.T("guard.action.flagged")
case "redacted":
return l.T("guard.action.redacted")
case "blocked":
return l.T("guard.action.blocked")
}
return action
}