235 lines
7.8 KiB
Go
235 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"homehub/internal/debugtrace"
|
|
"homehub/internal/store"
|
|
)
|
|
|
|
type captureWriter struct {
|
|
http.ResponseWriter
|
|
status int
|
|
buf bytes.Buffer
|
|
max int
|
|
}
|
|
|
|
func (w *captureWriter) WriteHeader(code int) {
|
|
if w.status == 0 {
|
|
w.status = code
|
|
}
|
|
w.ResponseWriter.WriteHeader(code)
|
|
}
|
|
func (w *captureWriter) Write(p []byte) (int, error) {
|
|
if w.status == 0 {
|
|
w.status = http.StatusOK
|
|
}
|
|
if w.buf.Len() < w.max {
|
|
remain := w.max - w.buf.Len()
|
|
if remain > len(p) {
|
|
remain = len(p)
|
|
}
|
|
_, _ = w.buf.Write(p[:remain])
|
|
}
|
|
return w.ResponseWriter.Write(p)
|
|
}
|
|
|
|
func (a *app) traceHTTP(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if a.debug == nil || !a.debug.Enabled() || !strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/api/debug") {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if (r.URL.Path == "/api/state" || r.URL.Path == "/api/health") && !a.debug.Config().LogHTTPState {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
ctx, traceID := debugtrace.EnsureTrace(r.Context(), "http")
|
|
r = r.WithContext(ctx)
|
|
w.Header().Set("X-JARVIS-Trace-ID", traceID)
|
|
started := time.Now()
|
|
var requestData any = map[string]any{"method": r.Method, "path": r.URL.Path, "query": r.URL.Query()}
|
|
ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
if r.Body != nil && (ct == "application/json" || ct == "text/plain" || ct == "application/x-www-form-urlencoded") {
|
|
raw, _ := io.ReadAll(io.LimitReader(r.Body, 512<<10))
|
|
r.Body.Close()
|
|
r.Body = io.NopCloser(bytes.NewReader(raw))
|
|
if len(raw) > 0 {
|
|
var v any
|
|
if json.Unmarshal(raw, &v) == nil {
|
|
requestData = map[string]any{"method": r.Method, "path": r.URL.Path, "query": r.URL.Query(), "body": v}
|
|
} else {
|
|
requestData = map[string]any{"method": r.Method, "path": r.URL.Path, "query": r.URL.Query(), "body_text": string(raw)}
|
|
}
|
|
}
|
|
} else if strings.HasPrefix(ct, "multipart/") {
|
|
requestData = map[string]any{"method": r.Method, "path": r.URL.Path, "content_type": ct, "content_length": r.ContentLength, "note": "multipart raw bytes intentionally not logged"}
|
|
}
|
|
a.debug.Record(ctx, "http", "request", "input", "info", requestData, nil, 0)
|
|
cw := &captureWriter{ResponseWriter: w, max: 512 << 10}
|
|
next.ServeHTTP(cw, r)
|
|
respData := map[string]any{"status": cw.status, "content_type": cw.Header().Get("Content-Type")}
|
|
body := bytes.TrimSpace(cw.buf.Bytes())
|
|
if len(body) > 0 {
|
|
var v any
|
|
if json.Unmarshal(body, &v) == nil {
|
|
respData["body"] = v
|
|
} else if strings.HasPrefix(cw.Header().Get("Content-Type"), "text/") {
|
|
respData["body_text"] = string(body)
|
|
} else {
|
|
respData["body_bytes_captured"] = len(body)
|
|
}
|
|
}
|
|
a.debug.Record(ctx, "http", "response", "output", "info", respData, nil, time.Since(started))
|
|
})
|
|
}
|
|
|
|
func traceBackground(ctx context.Context) context.Context {
|
|
id := debugtrace.TraceID(ctx)
|
|
if id == "" {
|
|
return context.Background()
|
|
}
|
|
return debugtrace.WithTrace(context.Background(), id)
|
|
}
|
|
|
|
func (a *app) trace(ctx context.Context, component, stage, direction string, data any) {
|
|
if a.debug != nil {
|
|
a.debug.Record(ctx, component, stage, direction, "info", data, nil, 0)
|
|
}
|
|
}
|
|
func (a *app) traceErr(ctx context.Context, component, stage string, data any, err error, started time.Time) {
|
|
if a.debug != nil {
|
|
a.debug.Record(ctx, component, stage, "output", "error", data, err, time.Since(started))
|
|
}
|
|
}
|
|
func (a *app) traceTimed(ctx context.Context, component, stage, direction string, data any, started time.Time) {
|
|
if a.debug != nil {
|
|
a.debug.Record(ctx, component, stage, direction, "info", data, nil, time.Since(started))
|
|
}
|
|
}
|
|
|
|
func (a *app) debugRuntimeState() map[string]any {
|
|
a.pendingMu.Lock()
|
|
var pending any
|
|
if a.pending != nil {
|
|
cp := *a.pending
|
|
cp.Items = append([]pendingItem(nil), a.pending.Items...)
|
|
pending = cp
|
|
}
|
|
a.pendingMu.Unlock()
|
|
a.recentMu.Lock()
|
|
var recent any
|
|
if a.recent != nil {
|
|
recent = *a.recent
|
|
}
|
|
a.recentMu.Unlock()
|
|
a.confirmMu.Lock()
|
|
var confirmation any
|
|
if a.confirm != nil {
|
|
cp := *a.confirm
|
|
cp.Arguments = map[string]any{}
|
|
for k, v := range a.confirm.Arguments {
|
|
cp.Arguments[k] = v
|
|
}
|
|
confirmation = cp
|
|
}
|
|
a.confirmMu.Unlock()
|
|
return map[string]any{"pending_command": pending, "pending_tool_confirmation": confirmation, "recent_mutation": recent}
|
|
}
|
|
|
|
func debugStateSnapshot(st store.State) map[string]any {
|
|
docs := make([]map[string]any, 0, len(st.Documents))
|
|
for _, d := range st.Documents {
|
|
docs = append(docs, map[string]any{
|
|
"id": d.ID, "name": d.Name, "mime": d.Mime, "tags": d.Tags,
|
|
"chunkCount": d.ChunkCount, "indexedAt": d.IndexedAt, "createdAt": d.CreatedAt,
|
|
"textChars": len([]rune(d.Text)),
|
|
})
|
|
}
|
|
return map[string]any{
|
|
"tasks": st.Tasks, "events": st.Events, "reminders": st.Reminders,
|
|
"groceries": st.Groceries, "recipes": st.Recipes, "mealPlans": st.MealPlans,
|
|
"kanban": st.Kanban, "documents": docs, "chat": st.Chat, "activity": st.Activity,
|
|
}
|
|
}
|
|
|
|
func (a *app) handleDebugExport(w http.ResponseWriter, r *http.Request) {
|
|
if a.debug == nil || !a.debug.Enabled() {
|
|
http.Error(w, "Debug tracing ist deaktiviert", 404)
|
|
return
|
|
}
|
|
limit := 5000
|
|
if v, _ := strconv.Atoi(r.URL.Query().Get("limit")); v > 0 && v <= 50000 {
|
|
limit = v
|
|
}
|
|
events, err := a.debug.Events(limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
st := a.db.Snapshot()
|
|
ms := a.ai.Status()
|
|
runtimeState := a.debugRuntimeState()
|
|
manifest := map[string]any{
|
|
"schema": "jarvis.debug.export.v3", "generated_at": time.Now().In(a.timezone).Format(time.RFC3339), "timezone": a.timezone.String(),
|
|
"events": len(events), "ollama": ms, "context": a.context, "debug": a.debug.Config(), "rag_chunks": a.rag.Count(),
|
|
"state_summary": map[string]any{"events": len(st.Events), "tasks": len(st.Tasks), "reminders": len(st.Reminders), "groceries": len(st.Groceries), "recipes": len(st.Recipes), "meal_plans": len(st.MealPlans), "kanban": len(st.Kanban), "documents": len(st.Documents), "chat_turns": len(st.Chat)},
|
|
"runtime": runtimeState,
|
|
"agent": map[string]any{"mode": "skill-mesh-agent", "max_steps": a.context.AgentMaxSteps, "tools": toolNames(a.skills.Definitions())},
|
|
"skills": a.skills.Status(),
|
|
"mesh": a.mesh.Status(),
|
|
"docker_skills": a.docker.Status(r.Context()),
|
|
"home_state": debugStateSnapshot(st),
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=jarvis-debug-%s.json", time.Now().Format("20060102-150405")))
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte("{\n \"manifest\": "))
|
|
mb, _ := json.MarshalIndent(manifest, " ", " ")
|
|
_, _ = w.Write(mb)
|
|
_, _ = w.Write([]byte(",\n \"events\": [\n"))
|
|
for i, e := range events {
|
|
if i > 0 {
|
|
_, _ = w.Write([]byte(",\n"))
|
|
}
|
|
_, _ = w.Write([]byte(" "))
|
|
_, _ = w.Write(e)
|
|
}
|
|
_, _ = w.Write([]byte("\n ]\n}\n"))
|
|
}
|
|
func (a *app) handleDebugExportJSONL(w http.ResponseWriter, r *http.Request) {
|
|
if a.debug == nil || !a.debug.Enabled() {
|
|
http.Error(w, "Debug tracing ist deaktiviert", 404)
|
|
return
|
|
}
|
|
limit := 10000
|
|
if v, _ := strconv.Atoi(r.URL.Query().Get("limit")); v > 0 && v <= 100000 {
|
|
limit = v
|
|
}
|
|
w.Header().Set("Content-Type", "application/x-ndjson")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=jarvis-debug-%s.jsonl", time.Now().Format("20060102-150405")))
|
|
if err := a.debug.WriteJSONL(w, limit); err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
}
|
|
}
|
|
func (a *app) handleDebugClear(w http.ResponseWriter, r *http.Request) {
|
|
if a.debug == nil || !a.debug.Enabled() {
|
|
http.Error(w, "Debug tracing ist deaktiviert", 404)
|
|
return
|
|
}
|
|
if err := a.debug.Clear(); err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ok": true})
|
|
}
|