generated from mathias/template-go-web
Phase C: the CI stage now shows the latest run's per-JOB status (Lint/Test, Build, Deploy, …), each coloured by outcome, from the Gitea /actions/tasks per-job entries — replacing the static cd.yml job-id list when live. LatestRunJobs/RunNodes + RunSummary.State aggregate, all test-first. Version: injected at build via -ldflags from `git describe --tags` (CI checkout now fetch-depth:0 + --build-arg), served in /api/atlas.json, shown in the header. No more hand-maintained version label drifting from the git tag. Verified: build/vet/lint(0)/test green; version ldflag served correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
2.7 KiB
Go
108 lines
2.7 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"
|
|
"git.d-ma.be/mathias/cad-atlas/internal/gitea"
|
|
"git.d-ma.be/mathias/cad-atlas/internal/version"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
a.Version = version.Value
|
|
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 {
|
|
nodes := atlas.RunNodes(*ld.run)
|
|
for i := range a.Stages {
|
|
if a.Stages[i].Generate == "ci-jobs" {
|
|
a.Stages[i].Nodes = 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.RunSummary
|
|
}
|
|
|
|
// 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.LatestRunJobs(raw); err == nil {
|
|
d.run = &r
|
|
}
|
|
}
|
|
liveCache = d
|
|
return d
|
|
}
|