generated from mathias/template-go-web
feat(atlas): Phase B tail — live namespaces + trim inline fallback
NamespaceSummary (test-first) renders the substrate ns line from live cluster namespaces (system-filtered, sorted, capped). Handler now overlays both node specs and the ns line via a single cached liveOverlay(). Removed the inline SUBSTRATE/STAGES/NS data — /api/atlas.json is the single source; the page shows an error banner on fetch failure instead of stale data. GPU model naming intentionally NOT done: koala's node has no GPU product label (only nvidia.com/gpu count), so "1× GPU" is the API's truth — naming the RTX 5070 would need GPU-feature-discovery, out of scope. Verified: build/vet/lint(0)/test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -52,6 +53,47 @@ func HostsFromNodes(nodesJSON []byte) ([]Host, error) {
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// NamespaceSummary parses a Kubernetes `/api/v1/namespaces` list into a compact
|
||||
// "ns: a · b · c" line for the substrate, dropping system namespaces, sorting,
|
||||
// and capping the count (with "+N more" when it overflows).
|
||||
func NamespaceSummary(nsJSON []byte) (string, error) {
|
||||
var list struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(nsJSON, &list); err != nil {
|
||||
return "", fmt.Errorf("parse namespaces: %w", err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, it := range list.Items {
|
||||
n := it.Metadata.Name
|
||||
if strings.HasPrefix(n, "kube-") || n == "default" || n == "flux-system" {
|
||||
continue
|
||||
}
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
const maxShown = 12
|
||||
more := 0
|
||||
if len(names) > maxShown {
|
||||
more = len(names) - maxShown
|
||||
names = names[:maxShown]
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return "ns: (none)", nil
|
||||
}
|
||||
s := "ns: " + strings.Join(names, " · ")
|
||||
if more > 0 {
|
||||
s += " · +" + strconv.Itoa(more) + " more"
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// MergeSubstrate overlays live node specs onto the authored substrate: an
|
||||
// authored host is replaced by the live node of the same name (fresh specs),
|
||||
// authored-only machines (non-cluster: iguana/flamingo/piguard) are kept, and
|
||||
|
||||
@@ -2,6 +2,7 @@ package atlas_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
@@ -62,6 +63,41 @@ func TestMergeSubstrate_NoLiveReturnsAuthored(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceSummary_FiltersSystemSortsAndJoins(t *testing.T) {
|
||||
ns := []byte(`{"items":[
|
||||
{"metadata":{"name":"gitea"}},
|
||||
{"metadata":{"name":"kube-system"}},
|
||||
{"metadata":{"name":"ai-stack"}},
|
||||
{"metadata":{"name":"default"}},
|
||||
{"metadata":{"name":"flux-system"}},
|
||||
{"metadata":{"name":"brain"}}
|
||||
]}`)
|
||||
|
||||
got, err := atlas.NamespaceSummary(ns)
|
||||
if err != nil {
|
||||
t.Fatalf("NamespaceSummary: %v", err)
|
||||
}
|
||||
if want := "ns: ai-stack · brain · gitea"; got != want {
|
||||
t.Fatalf("summary = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceSummary_CapsWithMore(t *testing.T) {
|
||||
var items []string
|
||||
for i := 0; i < 15; i++ {
|
||||
items = append(items, `{"metadata":{"name":"app`+string(rune('a'+i))+`"}}`)
|
||||
}
|
||||
ns := []byte(`{"items":[` + strings.Join(items, ",") + `]}`)
|
||||
|
||||
got, err := atlas.NamespaceSummary(ns)
|
||||
if err != nil {
|
||||
t.Fatalf("NamespaceSummary: %v", err)
|
||||
}
|
||||
if !strings.HasSuffix(got, "· +3 more") {
|
||||
t.Fatalf("expected cap suffix, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostsFromNodes_ErrorsOnBadJSON(t *testing.T) {
|
||||
if _, err := atlas.HostsFromNodes([]byte("{not json")); err == nil {
|
||||
t.Fatal("expected error on bad JSON, got nil")
|
||||
|
||||
Reference in New Issue
Block a user