This commit is contained in:
+92
-10
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/example/glpi-ai-agent/internal/state"
|
||||
)
|
||||
|
||||
//go:embed templates/dashboard.html
|
||||
//go:embed templates/dashboard.html templates/diagnostics.html
|
||||
var files embed.FS
|
||||
|
||||
type KnowledgeManager interface {
|
||||
@@ -38,6 +38,7 @@ type KnowledgeManager interface {
|
||||
LoadStats() knowledgepkg.LoadStats
|
||||
InitStatus() knowledgepkg.InitStatus
|
||||
Ready() bool
|
||||
FindMetadata(string, int) []model.KnowledgeDoc
|
||||
}
|
||||
type FeedbackManager interface {
|
||||
Categories(context.Context) ([]model.Category, error)
|
||||
@@ -47,22 +48,32 @@ type FeedbackManager interface {
|
||||
LearningCount() int
|
||||
}
|
||||
|
||||
type DiagnosticsManager interface {
|
||||
DiagnoseRun(context.Context, string) (model.RunRecord, error)
|
||||
DiagnoseKnowledge(context.Context, string, string) (model.KnowledgeDiagnostic, error)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
cfg config.Config
|
||||
metrics *metrics.Metrics
|
||||
state *state.Store
|
||||
q *queue.Queue
|
||||
knowledge KnowledgeManager
|
||||
feedback FeedbackManager
|
||||
tpl *template.Template
|
||||
cfg config.Config
|
||||
metrics *metrics.Metrics
|
||||
state *state.Store
|
||||
q *queue.Queue
|
||||
knowledge KnowledgeManager
|
||||
feedback FeedbackManager
|
||||
diagnostics DiagnosticsManager
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func New(cfg config.Config, m *metrics.Metrics, s *state.Store, q *queue.Queue, k KnowledgeManager, f FeedbackManager) (*Server, error) {
|
||||
t, err := template.ParseFS(files, "templates/dashboard.html")
|
||||
t, err := template.ParseFS(files, "templates/*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Server{cfg: cfg, metrics: m, state: s, q: q, knowledge: k, feedback: f, tpl: t}, nil
|
||||
srv := &Server{cfg: cfg, metrics: m, state: s, q: q, knowledge: k, feedback: f, tpl: t}
|
||||
if d, ok := f.(DiagnosticsManager); ok {
|
||||
srv.diagnostics = d
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
@@ -70,6 +81,10 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /readyz", s.ready)
|
||||
mux.HandleFunc("GET /metrics", s.prom)
|
||||
mux.Handle("GET /", s.auth(http.HandlerFunc(s.dashboard)))
|
||||
mux.Handle("GET /diagnostics", s.auth(http.HandlerFunc(s.diagnosticsPage)))
|
||||
mux.Handle("GET /api/diagnostics/run/{id}", s.auth(http.HandlerFunc(s.diagnosticRun)))
|
||||
mux.Handle("GET /api/diagnostics/run/{id}/knowledge", s.auth(http.HandlerFunc(s.diagnosticKnowledge)))
|
||||
mux.Handle("GET /api/diagnostics/knowledge", s.auth(http.HandlerFunc(s.diagnosticKnowledgeSearch)))
|
||||
mux.Handle("GET /api/status", s.auth(http.HandlerFunc(s.status)))
|
||||
mux.Handle("GET /api/runs", s.auth(http.HandlerFunc(s.runs)))
|
||||
mux.Handle("GET /api/categories", s.auth(http.HandlerFunc(s.categories)))
|
||||
@@ -110,6 +125,73 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = s.tpl.ExecuteTemplate(w, "dashboard.html", map[string]any{"DryRun": s.cfg.DryRun, "AutoReply": s.cfg.AutoReply, "AutoCategory": s.cfg.AutoCategory, "CommunicationLanguage": s.cfg.CommunicationLanguage, "CommunicationStyle": s.cfg.CommunicationStyle})
|
||||
}
|
||||
func (s *Server) diagnosticsPage(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/diagnostics" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = s.tpl.ExecuteTemplate(w, "diagnostics.html", nil)
|
||||
}
|
||||
|
||||
func (s *Server) diagnosticRun(w http.ResponseWriter, r *http.Request) {
|
||||
if s.diagnostics == nil {
|
||||
http.Error(w, "diagnostics unavailable", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
run, err := s.diagnostics.DiagnoseRun(r.Context(), strings.TrimSpace(r.PathValue("id")))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
respondJSON(w, run)
|
||||
}
|
||||
|
||||
func (s *Server) diagnosticKnowledge(w http.ResponseWriter, r *http.Request) {
|
||||
if s.diagnostics == nil {
|
||||
http.Error(w, "diagnostics unavailable", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
kbID := strings.TrimSpace(r.URL.Query().Get("knowledge_id"))
|
||||
if kbID == "" {
|
||||
http.Error(w, "knowledge_id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d, err := s.diagnostics.DiagnoseKnowledge(r.Context(), strings.TrimSpace(r.PathValue("id")), kbID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
respondJSON(w, d)
|
||||
}
|
||||
|
||||
func (s *Server) diagnosticKnowledgeSearch(w http.ResponseWriter, r *http.Request) {
|
||||
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
|
||||
limit := 30
|
||||
if n, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && n > 0 && n <= 100 {
|
||||
limit = n
|
||||
}
|
||||
type item struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Source string `json:"source"`
|
||||
AutoReply bool `json:"auto_reply"`
|
||||
Unmapped []string `json:"unmapped_categories,omitempty"`
|
||||
}
|
||||
out := make([]item, 0, limit)
|
||||
for _, d := range s.knowledge.List() {
|
||||
hay := strings.ToLower(d.ID + " " + d.Title + " " + strings.Join(d.Keywords, " ") + " " + strings.Join(d.ExternalCategories, " "))
|
||||
if q != "" && !strings.Contains(hay, q) {
|
||||
continue
|
||||
}
|
||||
out = append(out, item{ID: d.ID, Title: d.Title, Source: d.Source, AutoReply: d.AutoReply, Unmapped: append([]string(nil), d.UnmappedExternalCategories...)})
|
||||
if len(out) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
respondJSON(w, out)
|
||||
}
|
||||
|
||||
func (s *Server) status(w http.ResponseWriter, r *http.Request) {
|
||||
g, o := s.metrics.Health()
|
||||
kbOK, kbDocs, kbLastSync, kbLastErr := s.metrics.GLPIKBStatus()
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestExtractTicketID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDashboardTemplateParses(t *testing.T) {
|
||||
if _, err := template.ParseFS(files, "templates/dashboard.html"); err != nil {
|
||||
if _, err := template.ParseFS(files, "templates/*.html"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -171,3 +171,15 @@ func TestDashboardContainsControlCenterSections(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticsTemplateAndKnowledgeSearch(t *testing.T) {
|
||||
h, _ := newKnowledgeTestServer(t)
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/diagnostics", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("diagnostics = %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !bytes.Contains(rr.Body.Bytes(), []byte("Entscheidungsdiagnose")) {
|
||||
t.Fatal("diagnostics page missing title")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ button,input,textarea,select{font:inherit}button{color:inherit}.app{display:grid
|
||||
<button class="nav-btn" data-view="knowledge"><span class="nav-icon">▤</span>Knowledge Base</button>
|
||||
<button class="nav-btn" data-view="learning"><span class="nav-icon">◇</span>Lernen</button>
|
||||
<button class="nav-btn" data-view="settings"><span class="nav-icon">⚙</span>Konfiguration</button>
|
||||
<a class="nav-btn" href="/diagnostics" style="text-decoration:none"><span class="nav-icon">⌁</span>Diagnose</a>
|
||||
</nav>
|
||||
<div class="side-bottom"><div class="modebox" id="sideMode">Status wird geladen…</div></div>
|
||||
</aside>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user