5aaecd2a53
Traces were written as facts. A single Praxis action wrote several of them, so machine-rate rows crowded out the bounded fact readers that humans and evaluation consume. The habit profile window of 2000 facts and the memeval snapshot both filled with call records instead of what Maven learned about the owner. Traces now go to ecosystem_traces, with correlation, causation, duration and HTTP status as columns, pruned to the most recent 5000. The new reader is exposed over IPC and rendered as the Calls card on the ecosystem page, so it is a table someone actually looks at. Found in review of #84.
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
)
|
|
|
|
// The three sibling services Maven coordinates are headless JSON APIs (no web UI
|
|
// of their own — arch §16: mavweb IS their human surface). This page is that
|
|
// surface: a read-only observability panel that GETs each sibling's list
|
|
// endpoints and renders them. Never mutates — enabling/executing stays on the
|
|
// authed /tools + voice paths, never here.
|
|
|
|
// ecoURLs holds the sibling base URLs; empty ⇒ that panel shows "not configured".
|
|
type ecoURLs struct {
|
|
nexus, praxis, hexis string
|
|
}
|
|
|
|
var ecoClient = &http.Client{Timeout: 6 * time.Second}
|
|
|
|
// getEco GETs path off base and decodes the JSON array into out. Returns a
|
|
// human error string (empty on success) — honest per-panel state, no spinner.
|
|
func getEco(ctx context.Context, base, path string, out any) string {
|
|
if base == "" {
|
|
return "not configured"
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil)
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
resp, err := ecoClient.Do(req)
|
|
if err != nil {
|
|
return "unreachable"
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Sprintf("http %d", resp.StatusCode)
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
return "bad json"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Minimal projections of each sibling's list payload — only the columns the
|
|
// panel shows. Extra JSON fields are ignored.
|
|
type ecoEntity struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
DisplayName string `json:"display_name"`
|
|
State string `json:"state"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ecoItem struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
Importance int `json:"importance"`
|
|
LastSeenAt time.Time `json:"last_seen_at"`
|
|
}
|
|
|
|
type ecoCap struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Provider string `json:"provider"`
|
|
Operation string `json:"operation"`
|
|
Risk string `json:"risk"`
|
|
ReadOnly bool `json:"read_only"`
|
|
}
|
|
|
|
type ecoPanel[T any] struct {
|
|
Rows []T
|
|
Err string
|
|
}
|
|
|
|
type ecoData struct {
|
|
Nexus ecoPanel[ecoEntity]
|
|
Praxis ecoPanel[ecoItem]
|
|
Hexis ecoPanel[ecoCap]
|
|
Calls ecoPanel[ipc.EcosystemTrace]
|
|
}
|
|
|
|
// handleEcosystem renders the three sibling panels plus Maven's own log of the
|
|
// calls she made to them. The call log comes from core, not from the siblings:
|
|
// it is what Maven saw, including the hops that never got an answer.
|
|
func handleEcosystem(w http.ResponseWriter, r *http.Request, urls ecoURLs, core ipc.CoreAPI) {
|
|
ctx := r.Context()
|
|
var d ecoData
|
|
var wg sync.WaitGroup
|
|
wg.Add(3)
|
|
go func() {
|
|
defer wg.Done()
|
|
d.Nexus.Err = getEco(ctx, urls.nexus, "/api/v1/entities?limit=50", &d.Nexus.Rows)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
d.Praxis.Err = getEco(ctx, urls.praxis, "/api/v1/items?limit=50", &d.Praxis.Rows)
|
|
}()
|
|
go func() { defer wg.Done(); d.Hexis.Err = getEco(ctx, urls.hexis, "/api/v1/capabilities", &d.Hexis.Rows) }()
|
|
wg.Wait()
|
|
|
|
if core == nil {
|
|
d.Calls.Err = "not configured"
|
|
} else if rows, err := core.RecentEcosystemTraces(ctx, 50); err != nil {
|
|
log.Printf("ecosystem traces: %v", err)
|
|
d.Calls.Err = "core read failed"
|
|
} else {
|
|
d.Calls.Rows = rows
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := ecosystemTmpl.Execute(w, d); err != nil {
|
|
log.Printf("ecosystem render: %v", err)
|
|
}
|
|
}
|