Files
cad-atlas/internal/web/handler.go
T
mathiasandClaude Opus 4.8 633ba153f2
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
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>
2026-07-20 00:56:40 +02:00

91 lines
2.3 KiB
Go

package web
import (
_ "embed"
"encoding/json"
"net/http"
"sync"
"time"
cadatlas "git.d-ma.be/mathias/cad-atlas"
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
"git.d-ma.be/mathias/cad-atlas/internal/cluster"
)
// atlasHTML is the Phase-A/B static shell. It fetches /api/atlas.json at load
// and renders from that data (no inline arrays), so the content is sourced.
//
//go:embed static/cad-atlas.html
var atlasHTML []byte
// NewHandler serves the CAD Atlas: the shell at "/", and the sourced data at
// "/api/atlas.json" (authored data + CI stage generated from the real cd.yml +
// substrate overlaid from the live cluster when reachable).
func NewHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(atlasHTML)
})
mux.HandleFunc("/api/atlas.json", func(w http.ResponseWriter, r *http.Request) {
a, err := atlas.Default(cadatlas.CDWorkflow)
if err != nil {
http.Error(w, "atlas build failed", http.StatusInternalServerError)
return
}
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)
})
return mux
}
// 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 (
liveMu sync.Mutex
liveCache liveOverlayData
liveAt time.Time
)
const liveTTL = 30 * time.Second
func liveOverlay() liveOverlayData {
liveMu.Lock()
defer liveMu.Unlock()
if !liveAt.IsZero() && time.Since(liveAt) < liveTTL {
return liveCache
}
liveAt = time.Now()
var d liveOverlayData
if raw, err := cluster.Nodes(); err == nil {
if hosts, err := atlas.HostsFromNodes(raw); err == nil {
d.hosts = hosts
}
}
if raw, err := cluster.Namespaces(); err == nil {
if s, err := atlas.NamespaceSummary(raw); err == nil {
d.ns = s
}
}
liveCache = d
return d
}