Files
cad-atlas/internal/web/handler.go
T
mathiasandClaude Opus 4.8 39fd9b9adc
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s
feat(atlas): hybrid substrate — live specs for cluster nodes, authored rest
MergeSubstrate overlays live node specs onto the authored machine list by name:
cluster nodes (koala) get fresh live specs, non-cluster machines (iguana/flamingo/
piguard) stay authored, new live nodes are appended. Test-first. Restores the full
homelab machine list while keeping cluster nodes truthful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 00:47:53 +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 = atlas.MergeSubstrate(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
}