Files
glpi-neural-brain/internal/web/server.go
2026-08-04 05:28:51 +02:00

222 lines
7.2 KiB
Go

package web
import (
"context"
"embed"
"encoding/json"
"io"
"io/fs"
"net/http"
"strings"
"time"
"github.com/local/glpi-neural-brain/internal/activity"
"github.com/local/glpi-neural-brain/internal/engine"
"github.com/local/glpi-neural-brain/internal/graph"
"github.com/local/glpi-neural-brain/internal/model"
)
//go:embed static/*
var assets embed.FS
type Server struct {
Engine *engine.Engine
Graph *graph.Store
Broker *activity.Broker
APIKey string
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/status", s.handleStatus)
mux.HandleFunc("GET /api/graph", s.handleGraph)
mux.HandleFunc("GET /api/analysis", s.handleAnalysis)
mux.HandleFunc("GET /api/runtime-settings", s.handleGetRuntimeSettings)
mux.HandleFunc("PUT /api/runtime-settings", s.handleSetRuntimeSettings)
mux.HandleFunc("GET /api/categories", s.handleCategories)
mux.HandleFunc("GET /api/stream", s.Broker.ServeSSE)
mux.HandleFunc("POST /api/query", s.handleQuery)
mux.HandleFunc("POST /api/events", s.handleEvent)
mux.HandleFunc("POST /api/reindex", s.handleReindex)
mux.HandleFunc("POST /api/enrich", s.handleEnrich)
mux.HandleFunc("POST /api/glpi-kb/sync", s.handleGLPIKBSync)
mux.HandleFunc("POST /api/flush", s.handleFlush)
sub, _ := fs.Sub(assets, "static")
mux.Handle("GET /", http.FileServer(http.FS(sub)))
return s.headers(mux)
}
func (s *Server) headers(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
next.ServeHTTP(w, r)
})
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, s.Engine.Status())
}
func (s *Server) handleGraph(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, s.Graph.Snapshot())
}
func (s *Server) handleAnalysis(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, s.Graph.Analyze())
}
func (s *Server) handleGetRuntimeSettings(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Engine.RuntimeSettings())
}
func (s *Server) handleSetRuntimeSettings(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
var settings engine.RuntimeSettings
if err := decode(r, &settings); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
updated, err := s.Engine.SetRuntimeSettings(settings)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, updated)
}
func (s *Server) handleCategories(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"categories": s.Engine.Categories()})
}
func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
var req model.QueryRequest
if err := decode(r, &req); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
ctx, cancel := contextTimeout(r, 8*time.Minute)
defer cancel()
out, err := s.Engine.Query(ctx, req.Query)
if err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, out)
}
func (s *Server) handleEvent(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
var in model.ExternalEvent
if err := decode(r, &in); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
var ids []string
for _, h := range in.Hits {
if n, ok := s.Graph.LookupExternal(h.ID); ok {
ids = append(ids, n.ID)
}
}
s.Broker.Publish(model.Activity{Type: nonempty(in.Type, "external.event"), Source: nonempty(in.Source, "external"), Phase: "external", Query: in.Query, Message: in.Message, NodeIDs: ids, EdgeIDs: s.Graph.ConnectingEdges(ids), Strength: .9, Metadata: in.Metadata})
writeJSON(w, 202, map[string]any{"ok": true, "resolved_nodes": len(ids)})
}
func (s *Server) handleReindex(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Minute)
defer cancel()
if err := s.Engine.Scan(ctx); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, s.Engine.Status())
}
func (s *Server) handleGLPIKBSync(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Minute)
defer cancel()
if err := s.Engine.SyncGLPIKB(ctx); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, s.Engine.Status())
}
func (s *Server) handleFlush(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
ctx, cancel := contextTimeout(r, 2*time.Minute)
defer cancel()
if err := s.Engine.Flush(ctx); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, s.Engine.Status())
}
func (s *Server) handleEnrich(w http.ResponseWriter, r *http.Request) {
if !s.authorized(r) {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
if r.URL.Query().Get("async") == "1" {
if !s.Engine.ThinkingEnabled() {
writeJSON(w, http.StatusConflict, map[string]any{"error": "AI-THINK is disabled by runtime settings", "status": s.Engine.Status()})
return
}
if !s.Engine.RequestEnrich("manual") {
writeJSON(w, http.StatusConflict, map[string]any{"error": "AI-THINK is already running or queued", "status": s.Engine.Status()})
return
}
writeJSON(w, http.StatusAccepted, map[string]any{"ok": true, "queued": true, "status": s.Engine.Status()})
return
}
ctx, cancel := contextTimeout(r, 10*time.Minute)
defer cancel()
if err := s.Engine.EnrichOne(ctx); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, s.Engine.Status())
}
func (s *Server) authorized(r *http.Request) bool {
if s.APIKey == "" {
return true
}
v := strings.TrimSpace(r.Header.Get("Authorization"))
return v == "Bearer "+s.APIKey || r.Header.Get("X-Brain-Key") == s.APIKey
}
func decode(r *http.Request, v any) error {
return json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(v)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func nonempty(a, b string) string {
if strings.TrimSpace(a) != "" {
return a
}
return b
}
func contextTimeout(r *http.Request, d time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(r.Context(), d)
}