From ae6d0eb30e41c9b0e6cc24d6a4ffdfde4927403c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 08:03:39 +0000 Subject: [PATCH] Fix console fetch, persist API log to disk Console: switched from FormData to URL-encoded POST body. FormData sends multipart which r.ParseForm() can't read, causing empty cmd and no response. API log: persisted to api_log.json every 10 seconds. Loaded on startup. Capped at 500 entries. Request/response bodies excluded from persistence (too large). Survives server restarts. https://claude.ai/code/session_01GRGLA9yj7BpqKiyi6xFwnm --- admin/console.go | 4 +--- internal/app/apilog.go | 48 +++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/admin/console.go b/admin/console.go index 9fc95e73..a7cc8bec 100644 --- a/admin/console.go +++ b/admin/console.go @@ -79,9 +79,7 @@ func ConsoleHandler(w http.ResponseWriter, r *http.Request) { hi=-1; out.innerHTML+='> '+esc(cmd)+'\n'; input.value=''; - var fd=new FormData(); - fd.append('cmd',cmd); - fetch('/admin/console',{method:'POST',body:fd,headers:{'Accept':'application/json'}}) + fetch('/admin/console',{method:'POST',body:'cmd='+encodeURIComponent(cmd),headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'}}) .then(function(r){return r.json()}) .then(function(j){ out.innerHTML+=esc(j.output)+'\n'; diff --git a/internal/app/apilog.go b/internal/app/apilog.go index 74f42775..f8e79c7f 100644 --- a/internal/app/apilog.go +++ b/internal/app/apilog.go @@ -1,32 +1,55 @@ package app import ( + "encoding/json" "sync" "time" + + "mu/internal/data" ) -const apiLogMaxEntries = 200 +const apiLogMaxEntries = 500 // APILogEntry records a single external API call. type APILogEntry struct { - Time time.Time - Service string - Method string - URL string - Status int - Duration time.Duration - Error string - RequestBody string - ResponseBody string + Time time.Time `json:"time"` + Service string `json:"service"` + Method string `json:"method"` + URL string `json:"url"` + Status int `json:"status"` + Duration time.Duration `json:"duration"` + Error string `json:"error,omitempty"` + RequestBody string `json:"-"` // not persisted — too large + ResponseBody string `json:"-"` // not persisted — too large } var ( apiLogMu sync.Mutex apiLogEntries []*APILogEntry + apiLogDirty bool ) -// RecordAPICall appends an external API call record to the in-memory log. -// When the log exceeds apiLogMaxEntries the oldest entry is dropped. +func init() { + b, err := data.LoadFile("api_log.json") + if err == nil && len(b) > 0 { + json.Unmarshal(b, &apiLogEntries) + } + // Start background saver + go func() { + for { + time.Sleep(10 * time.Second) + apiLogMu.Lock() + if apiLogDirty { + data.SaveJSON("api_log.json", apiLogEntries) + apiLogDirty = false + } + apiLogMu.Unlock() + } + }() +} + +// RecordAPICall appends an external API call record. +// Persisted to disk every 10 seconds, capped at 500 entries. func RecordAPICall(service, method, url string, status int, duration time.Duration, callErr error, reqBody, respBody string) { entry := &APILogEntry{ Time: time.Now(), @@ -46,6 +69,7 @@ func RecordAPICall(service, method, url string, status int, duration time.Durati if len(apiLogEntries) > apiLogMaxEntries { apiLogEntries = apiLogEntries[len(apiLogEntries)-apiLogMaxEntries:] } + apiLogDirty = true apiLogMu.Unlock() }