Diagnose impllementiert
release-tag / release-image (push) Successful in 1m34s

This commit is contained in:
2026-07-29 22:19:43 +02:00
parent a10225d74c
commit 7ec10c490b
74 changed files with 1992 additions and 164 deletions
+92 -10
View File
@@ -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()
+13 -1
View File
@@ -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")
}
}
+1
View File
@@ -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