feat(atlas): Phase B tail — live namespaces + trim inline fallback
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s

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:
2026-07-20 00:56:40 +02:00
co-authored by Claude Opus 4.8
parent 39fd9b9adc
commit 633ba153f2
5 changed files with 131 additions and 77 deletions
+36 -24
View File
@@ -37,8 +37,12 @@ func NewHandler() http.Handler {
http.Error(w, "atlas build failed", http.StatusInternalServerError)
return
}
if live := liveSubstrate(); len(live) > 0 {
a.Substrate = atlas.MergeSubstrate(a.Substrate, live)
ld := liveOverlay()
if len(ld.hosts) > 0 {
a.Substrate = atlas.MergeSubstrate(a.Substrate, ld.hosts)
}
if ld.ns != "" {
a.NS = "Tailscale mesh · " + ld.ns
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(a)
@@ -46,33 +50,41 @@ func NewHandler() http.Handler {
return mux
}
// substrate cache: query the cluster at most once per TTL; fall back to the
// authored substrate (returns nil) whenever the cluster is unreachable.
// liveOverlay holds cluster-sourced substrate facts (node specs + namespace line).
type liveOverlayData struct {
hosts []atlas.Host
ns string
}
// live cache: query the cluster at most once per TTL; fall back to the authored
// substrate/ns (zero values) whenever the cluster is unreachable.
var (
subMu sync.Mutex
subCache []atlas.Host
subAt time.Time
liveMu sync.Mutex
liveCache liveOverlayData
liveAt time.Time
)
const substrateTTL = 30 * time.Second
const liveTTL = 30 * time.Second
func liveSubstrate() []atlas.Host {
subMu.Lock()
defer subMu.Unlock()
if !subAt.IsZero() && time.Since(subAt) < substrateTTL {
return subCache
func liveOverlay() liveOverlayData {
liveMu.Lock()
defer liveMu.Unlock()
if !liveAt.IsZero() && time.Since(liveAt) < liveTTL {
return liveCache
}
subAt = time.Now()
raw, err := cluster.Nodes()
if err != nil {
subCache = nil
return nil
liveAt = time.Now()
var d liveOverlayData
if raw, err := cluster.Nodes(); err == nil {
if hosts, err := atlas.HostsFromNodes(raw); err == nil {
d.hosts = hosts
}
}
hosts, err := atlas.HostsFromNodes(raw)
if err != nil {
subCache = nil
return nil
if raw, err := cluster.Namespaces(); err == nil {
if s, err := atlas.NamespaceSummary(raw); err == nil {
d.ns = s
}
}
subCache = hosts
return hosts
liveCache = d
return d
}