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 // Off is the guardrails turned off (guard.enabled: false). Off bool 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, Rule 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 func() guard.Config) { p.hasGuard, p.guardLibrary = true, library mux.HandleFunc("GET /admin/guard", p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { ctx, l := r.Context(), langOf(r) lib := library() off := !lib.On() d := guardPageData{Nav: p.navFor(r, admin, "guard"), Version: p.version, Window: r.URL.Query().Get("window"), Windows: []string{"24h", "7d", "30d"}, Default: lib.Default, Off: off} since, ok := guardWindows[d.Window] if !ok { d.Window, since = "7d", guardWindows["7d"] } for name := range lib.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, Rule: e.Rule, 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 }