Files
Maven/cmd/mavweb/ecosystem.go
claude 12c2ae1d17 mavweb: the last two pages leave Go (V-409)
CLAUDE.md says every page is its own embedded .html file next to main.go,
and that no page markup lives in Go. Two pages were still Go string
constants: passkeyPageHTML in webauthn.go and modelsHTML in models.go. They
are now passkey.html and models.html, embedded.

The identifiers keep their names, so passkey_prf_test.go still reads
passkeyPageHTML and still asserts on the same bytes.

Both templates now build through parsePage, and Page and handleModels render
through renderPage. handleEcosystem did too and now does the same.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 01:29:08 +04:00

122 lines
3.3 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
}
renderPage(w, ecosystemTmpl, d)
}