12c2ae1d17
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>
122 lines
3.3 KiB
Go
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)
|
|
}
|