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 }