feat(stats): gateway request stats/audit API + polish scratch sort animations (link chain, reset, cleanup)

This commit is contained in:
root
2026-08-08 13:34:40 +08:00
parent 31eefe9288
commit dec03238dd
5 changed files with 646 additions and 131 deletions

View File

@ -5,6 +5,7 @@
package gateway
import (
"context"
"embed"
"encoding/json"
"fmt"
@ -25,6 +26,7 @@ type Gateway struct {
core *core.Core
apiKeys map[string]bool
ui http.Handler
stats *Stats
}
func New(c *core.Core, gatewayKeys []string) (*Gateway, error) {
@ -42,6 +44,7 @@ func New(c *core.Core, gatewayKeys []string) (*Gateway, error) {
core: c,
apiKeys: keys,
ui: http.FileServer(http.FS(sub)),
stats: NewStats(3000),
}, nil
}
@ -72,6 +75,8 @@ func (g *Gateway) routes(w http.ResponseWriter, r *http.Request) {
g.handleChat(w, r)
case r.URL.Path == "/api/status":
g.handleStatusAPI(w, r)
case r.URL.Path == "/api/stats" || strings.HasPrefix(r.URL.Path, "/api/stats/"):
g.handleStatsAPI(w, r)
case r.URL.Path == "/login":
g.handleLogin(w, r)
case r.URL.Path == "/api/login":
@ -128,10 +133,31 @@ func (g *Gateway) auth(next http.Handler) http.Handler {
http.Redirect(w, r, "/login?continue="+url.QueryEscape(r.URL.Path), http.StatusFound)
return
}
next.ServeHTTP(w, r)
next.ServeHTTP(w, r.WithContext(withKey(r.Context(), key)))
})
}
// keyCtxKey is the context key carrying the authenticated gateway key.
type keyCtxKey struct{}
func withKey(ctx context.Context, key string) context.Context {
return context.WithValue(ctx, keyCtxKey{}, key)
}
// reqKey returns the authenticated gateway key id (masked suffix for display).
func reqKey(ctx context.Context) string {
k, _ := ctx.Value(keyCtxKey{}).(string)
return k
}
// keyID returns a short stable id for a gateway key (last 6 chars).
func keyID(k string) string {
if len(k) <= 6 {
return k
}
return "***" + k[len(k)-6:]
}
// isAPIPath reports whether the request targets a JSON API endpoint that
// should answer 401 instead of redirecting to the login page.
func isAPIPath(p string) bool {