Files
cad-atlas/internal/web/handler.go
T
mathiasandClaude Opus 4.8 74dffc1ff3
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 0s
feat(atlas): Phase B — substrate from the live cluster
The substrate machines are now rendered from the live k3s nodes (arch, cpu,
memory, GPU, k3s version), read in-cluster via the mounted ServiceAccount with a
stdlib HTTP client (no client-go) and a 30s cache; falls back to the authored
substrate whenever the cluster is unreachable. Placement per the homelab decision
"in-cluster only for in-workload ops" — a running web app rendering live data fits.

HostsFromNodes parser built test-first. RBAC (read-only node-reader SA) shipped in
infra k3s/apps/cad-atlas/.

Verified: go build/vet/lint(0)/test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 00:43:20 +02:00

79 lines
2.0 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
}
if live := liveSubstrate(); len(live) > 0 {
a.Substrate = live
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(a)
})
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.
var (
subMu sync.Mutex
subCache []atlas.Host
subAt time.Time
)
const substrateTTL = 30 * time.Second
func liveSubstrate() []atlas.Host {
subMu.Lock()
defer subMu.Unlock()
if !subAt.IsZero() && time.Since(subAt) < substrateTTL {
return subCache
}
subAt = time.Now()
raw, err := cluster.Nodes()
if err != nil {
subCache = nil
return nil
}
hosts, err := atlas.HostsFromNodes(raw)
if err != nil {
subCache = nil
return nil
}
subCache = hosts
return hosts
}