feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services' JSON APIs (Nexus entities, Praxis attention, Hexis capabilities), fetched concurrently with honest per-panel error states. Siblings stay headless — mavweb is their human surface (arch §16). Wired via mavweb -nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network. Fix mobile horizontal overflow across all pages: .content is a flex child with default min-width:auto, so it refused to shrink below the tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll instead of dragging the page sideways. Verified via CDP geometry check (scrollWidth === clientWidth at 430px). Also includes in-progress Ethos UI redesign, ecosystem deploy compose, and planning docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
func handleEcosystem(w http.ResponseWriter, r *http.Request, urls ecoURLs) {
|
||||
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()
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := ecosystemTmpl.Execute(w, d); err != nil {
|
||||
log.Printf("ecosystem render: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user