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" "git.d-ma.be/mathias/cad-atlas/internal/gitea" ) // 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 } if ld.run != nil { node := atlas.RunNode(*ld.run) for i := range a.Stages { if a.Stages[i].Generate == "ci-jobs" { a.Stages[i].Nodes = append([]atlas.Node{node}, a.Stages[i].Nodes...) } } } 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 run *atlas.Run } // 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 } } if raw, err := gitea.Runs(); err == nil { if r, err := atlas.LatestRun(raw); err == nil { d.run = &r } } liveCache = d return d }