generated from mathias/template-go-web
feat(atlas): Phase B — data-driven /api/atlas.json, CI stage generated from cd.yml
The atlas no longer hand-maintains its content. Authored data lives in one place (internal/atlas/atlas.json); the Go layer overlays sourced facts and serves the result at /api/atlas.json; the frontend fetches + renders (inline arrays kept only as an offline fallback). First generated source: the CI/CD stage's nodes are parsed from the repo's own .gitea/workflows/cd.yml — so the viz shows the pipeline that actually runs (guard/check/build/deploy), dropping the aspirational var-go/oath-gate node that isn't wired yet. That's the point: it can't drift from the real pipeline. New internal/atlas package (JobsFromWorkflow, Build) built test-first. Adds gopkg.in/yaml.v3 (justified: parsing the workflow YAML; stdlib has no YAML). Verified: go build/vet/lint(0)/test green; /api/atlas.json → 9 stages, CI = real jobs; frontend renders from the fetch (screenshot). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
// Package cadatlas embeds module-root build artifacts that live outside any
|
||||
// subpackage directory. go:embed cannot reach parent directories, so the CI
|
||||
// workflow — which the atlas parses to generate its CI/CD stage from the real
|
||||
// pipeline — is embedded here at the module root.
|
||||
package cadatlas
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// CDWorkflow is the repo's own Gitea Actions workflow, embedded at build time.
|
||||
//
|
||||
//go:embed .gitea/workflows/cd.yml
|
||||
var CDWorkflow []byte
|
||||
@@ -3,3 +3,5 @@ module git.d-ma.be/mathias/cad-atlas
|
||||
go 1.26
|
||||
|
||||
require github.com/a-h/templ v0.3.1020
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
@@ -2,3 +2,7 @@ github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"substrate": [
|
||||
{"n":"koala","k":"RTX 5070 · k3s control-plane · Gitea · LiteLLM :30401 · llama-swap :31234 · searxng"},
|
||||
{"n":"iguana","k":"M2 Ultra · Ollama / mlx"},
|
||||
{"n":"flamingo","k":"daily driver · ~/dev"},
|
||||
{"n":"piguard","k":"NGINX reverse-proxy · ntfy"}
|
||||
],
|
||||
"ns": "Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council",
|
||||
"stages": [
|
||||
{"no":"STAGE 00","title":"Signals","path":"→ mathias/signals","nodes":[
|
||||
{"t":"Applied AI Radar","d":"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).","tags":["cron · daily/weekly","→ signals #1–26+"]},
|
||||
{"t":"Manual capture","d":"claude.ai strategic drop · brain capture tool.","tags":["ad-hoc"]},
|
||||
{"t":"Aspirational surfaces","pill":"var(--dim)","d":"Telegram / voice / URL → inbox. NOT built.","tags":["gap"]}
|
||||
]},
|
||||
{"no":"STAGE 01","cls":"telos","title":"TELOS","path":"wiki/telos/","nodes":[
|
||||
{"t":"Intention substrate","pill":"var(--violet)","d":"Mission · goals · problems · strategies · status. Every downstream item traces to a goal.","tags":["brain_query wing=telos"]}
|
||||
]},
|
||||
{"no":"STAGE 02","title":"Strategic session","path":"claude.ai frontier + brain MCP","nodes":[
|
||||
{"t":"Design · ADRs · specs","d":"Human + frontier model. ISC acceptance criteria written here.","tags":["Define / converge"]},
|
||||
{"t":"🏛️ LLM Council","cls":"council","pill":"var(--violet)","d":"fan-out → anonymous cross-review → chairman synth. glm-4.7-flash · qwen36-35b · gemma4-31b (chair).","tags":["hard strategic Q","chat.d-ma.be"]},
|
||||
{"t":"Autoresearch Council","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]}
|
||||
]},
|
||||
{"no":"STAGE 03","title":"Spec → Gitea issue","path":"agent-ready contract","nodes":[
|
||||
{"t":"Contract enforced","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]},
|
||||
{"t":"Admission controller","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]},
|
||||
{"t":"⚖️ var-go Oath","cls":"oath","pill":"var(--gold)","d":"Acceptance contract embedded in the issue as a var fenced block. Exactly one — zero/multiple fail closed. Prose → typed steps; failures anchored to byte spans.","tags":["swedsl · var-go","defined here → enforced @06"]}
|
||||
]},
|
||||
{"no":"STAGE 04","cls":"gate","title":"Human dispatch gate","path":"the only checkpoint","nodes":[
|
||||
{"t":"Human triggers execution","cls":"gateway","pill":"var(--amber)","d":"Ratify proposed-plan + risk tier, then dispatch.","gate":true},
|
||||
{"t":"Session-Dispatch bridge","cls":"bridge","pill":"var(--blue)","d":"claude.ai MCP → gitea:workflow_run_trigger → cad-dispatch.yml → agentsquad. The final design→execution bridge.","tags":["workflow_dispatch"]}
|
||||
]},
|
||||
{"no":"STAGE 05","cls":"exec","title":"Execute · agentsquad","path":"koala · cmd/agentsquad-serve","nodes":[
|
||||
{"t":"Task API","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]},
|
||||
{"t":"Executor + reviewer loop","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true},
|
||||
{"t":"dma-cli · routing + scope","cls":"bridge","pill":"var(--blue)","d":"Harness-config arm: routes agents to the right LLM backend. Three-layer scope policy + confirmation gate = CAD guardrail.","tags":["backend routing","scope guardrail"]},
|
||||
{"t":"assessor-loop ledger","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]}
|
||||
]},
|
||||
{"no":"STAGE 06","title":"PR → CI","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs","nodes":[]},
|
||||
{"no":"STAGE 07","cls":"cd","title":"CD → pod","path":"Flux GitOps → k3s","nodes":[
|
||||
{"t":"Deploy on green","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]}
|
||||
]},
|
||||
{"no":"STAGE 08","cls":"telos","title":"Loop back","path":"→ TELOS (feedback bus)","nodes":[
|
||||
{"t":"Close the loop","pill":"var(--violet)","d":"session_log + attestation → brain. Score deploy outcome vs originating goal. (arc partly manual — improvement target.)","tags":["continuous"]}
|
||||
]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package atlas_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
|
||||
func TestBuild_OverlaysCIStageNodesFromWorkflow(t *testing.T) {
|
||||
atlasJSON := []byte(`{
|
||||
"substrate": [{"n":"koala","k":"gpu"}],
|
||||
"stages": [
|
||||
{"no":"STAGE 06","title":"PR → CI","generate":"ci-jobs","nodes":[]},
|
||||
{"no":"STAGE 01","title":"TELOS","nodes":[{"t":"Intention substrate"}]}
|
||||
]}`)
|
||||
workflow := []byte("jobs:\n check:\n a: 1\n build:\n b: 2\n deploy:\n c: 3\n")
|
||||
|
||||
a, err := atlas.Build(atlasJSON, workflow)
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
|
||||
// CI stage nodes generated from the workflow's jobs, in order.
|
||||
var titles []string
|
||||
for _, n := range a.Stages[0].Nodes {
|
||||
titles = append(titles, n.Title)
|
||||
}
|
||||
if want := []string{"check", "build", "deploy"}; !reflect.DeepEqual(titles, want) {
|
||||
t.Fatalf("CI stage nodes = %v, want %v", titles, want)
|
||||
}
|
||||
|
||||
// Non-generated stage is untouched.
|
||||
if a.Stages[1].Nodes[0].Title != "Intention substrate" {
|
||||
t.Fatalf("authored stage was altered: %+v", a.Stages[1])
|
||||
}
|
||||
|
||||
// Substrate preserved.
|
||||
if len(a.Substrate) != 1 || a.Substrate[0].Name != "koala" {
|
||||
t.Fatalf("substrate not preserved: %+v", a.Substrate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_ErrorsOnBadAtlasJSON(t *testing.T) {
|
||||
if _, err := atlas.Build([]byte("{not json"), []byte("jobs:\n x:\n a: 1\n")); err == nil {
|
||||
t.Fatal("expected error on bad atlas JSON, got nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package atlas
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// DataJSON is the authored atlas content (substrate + stage narrative).
|
||||
// Sourced/generated facts are overlaid onto it at Build time.
|
||||
//
|
||||
//go:embed atlas.json
|
||||
var DataJSON []byte
|
||||
|
||||
// Default builds the atlas from the embedded authored data, overlaying the
|
||||
// given CI workflow (for the stage marked generate:ci-jobs).
|
||||
func Default(workflow []byte) (Atlas, error) {
|
||||
return Build(DataJSON, workflow)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Host is a substrate machine/entry (koala, iguana, …).
|
||||
type Host struct {
|
||||
Name string `json:"n"`
|
||||
Spec string `json:"k"`
|
||||
}
|
||||
|
||||
// Node is a card within a stage.
|
||||
type Node struct {
|
||||
Title string `json:"t"`
|
||||
Desc string `json:"d,omitempty"`
|
||||
Pill string `json:"pill,omitempty"`
|
||||
Cls string `json:"cls,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Risk bool `json:"risk,omitempty"`
|
||||
Gate bool `json:"gate,omitempty"`
|
||||
}
|
||||
|
||||
// Stage is one column of the pipeline. When Generate is set, its Nodes are
|
||||
// derived from a source at Build time rather than taken from the authored data.
|
||||
type Stage struct {
|
||||
No string `json:"no"`
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Cls string `json:"cls,omitempty"`
|
||||
Generate string `json:"generate,omitempty"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
}
|
||||
|
||||
// Atlas is the full data model the frontend renders.
|
||||
type Atlas struct {
|
||||
Substrate []Host `json:"substrate"`
|
||||
NS string `json:"ns,omitempty"`
|
||||
Stages []Stage `json:"stages"`
|
||||
}
|
||||
|
||||
// Build unmarshals the authored atlas JSON and overlays generated facts from
|
||||
// real sources, so sourced parts can't drift. Currently: any stage marked
|
||||
// `"generate":"ci-jobs"` gets its Nodes replaced by the workflow's job list.
|
||||
func Build(atlasJSON, workflow []byte) (Atlas, error) {
|
||||
var a Atlas
|
||||
if err := json.Unmarshal(atlasJSON, &a); err != nil {
|
||||
return Atlas{}, fmt.Errorf("parse atlas data: %w", err)
|
||||
}
|
||||
for i := range a.Stages {
|
||||
if a.Stages[i].Generate != "ci-jobs" {
|
||||
continue
|
||||
}
|
||||
jobs, err := JobsFromWorkflow(workflow)
|
||||
if err != nil {
|
||||
return Atlas{}, fmt.Errorf("stage %s: %w", a.Stages[i].No, err)
|
||||
}
|
||||
nodes := make([]Node, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
nodes = append(nodes, Node{Title: j})
|
||||
}
|
||||
a.Stages[i].Nodes = nodes
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Package atlas builds the CAD Atlas data model, deriving parts from real
|
||||
// sources (the repo's own CI workflow, infra manifests, the live cluster)
|
||||
// so the visualization can't drift from reality.
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// JobsFromWorkflow extracts the job names, in document order, from a Gitea
|
||||
// Actions / GitHub Actions workflow YAML. Used to generate the CI/CD stage of
|
||||
// the atlas from the pipeline that actually runs, rather than hand-authoring it.
|
||||
func JobsFromWorkflow(workflow []byte) ([]string, error) {
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(workflow, &doc); err != nil {
|
||||
return nil, fmt.Errorf("parse workflow: %w", err)
|
||||
}
|
||||
if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("workflow: expected a top-level mapping")
|
||||
}
|
||||
jobs := mappingValue(doc.Content[0], "jobs")
|
||||
if jobs == nil {
|
||||
return nil, fmt.Errorf("workflow: no jobs block")
|
||||
}
|
||||
if jobs.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("workflow: jobs is not a mapping")
|
||||
}
|
||||
|
||||
// A mapping node stores keys and values as alternating Content entries;
|
||||
// keys are the even indices, in document order.
|
||||
names := make([]string, 0, len(jobs.Content)/2)
|
||||
for i := 0; i+1 < len(jobs.Content); i += 2 {
|
||||
names = append(names, jobs.Content[i].Value)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// mappingValue returns the value node for key in a YAML mapping node, or nil.
|
||||
func mappingValue(m *yaml.Node, key string) *yaml.Node {
|
||||
for i := 0; i+1 < len(m.Content); i += 2 {
|
||||
if m.Content[i].Value == key {
|
||||
return m.Content[i+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package atlas_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
|
||||
func TestJobsFromWorkflow_ReturnsJobNamesInDocumentOrder(t *testing.T) {
|
||||
workflow := []byte(`
|
||||
name: CD
|
||||
"on":
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
guard:
|
||||
runs-on: self-hosted
|
||||
check:
|
||||
needs: guard
|
||||
runs-on: self-hosted
|
||||
build:
|
||||
needs: [guard, check]
|
||||
runs-on: self-hosted
|
||||
deploy:
|
||||
needs: [guard, build]
|
||||
runs-on: self-hosted
|
||||
`)
|
||||
|
||||
jobs, err := atlas.JobsFromWorkflow(workflow)
|
||||
if err != nil {
|
||||
t.Fatalf("JobsFromWorkflow: %v", err)
|
||||
}
|
||||
|
||||
want := []string{"guard", "check", "build", "deploy"}
|
||||
if !reflect.DeepEqual(jobs, want) {
|
||||
t.Fatalf("jobs = %v, want %v", jobs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobsFromWorkflow_ErrorsOnMalformedYAML(t *testing.T) {
|
||||
if _, err := atlas.JobsFromWorkflow([]byte("jobs: [this is: not valid")); err == nil {
|
||||
t.Fatal("expected error on malformed YAML, got nil")
|
||||
}
|
||||
}
|
||||
+16
-8
@@ -1,20 +1,22 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
cadatlas "git.d-ma.be/mathias/cad-atlas"
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
|
||||
// atlasHTML is the Phase-A static hero visualization. Phase C replaces this
|
||||
// self-contained file with a Templ view hydrated from live CAD trace data
|
||||
// (assessor-loop ledger, session_log, Gitea run API, Flux events).
|
||||
// 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. Root ("/") returns the static atlas;
|
||||
// /api/hello is a leftover template probe kept until Phase C wires real endpoints.
|
||||
// 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).
|
||||
func NewHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -25,8 +27,14 @@ func NewHandler() http.Handler {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(atlasHTML)
|
||||
})
|
||||
mux.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = Hello("world").Render(context.Background(), w)
|
||||
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
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(a)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
|
||||
func TestRootServesAtlas(t *testing.T) {
|
||||
@@ -27,6 +30,43 @@ func TestRootServesAtlas(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtlasJSON_ServesAtlasWithGeneratedCIStage(t *testing.T) {
|
||||
srv := httptest.NewServer(NewHandler())
|
||||
defer srv.Close()
|
||||
|
||||
resp, err := http.Get(srv.URL + "/api/atlas.json")
|
||||
if err != nil {
|
||||
t.Fatalf("GET /api/atlas.json: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var a atlas.Atlas
|
||||
if err := json.NewDecoder(resp.Body).Decode(&a); err != nil {
|
||||
t.Fatalf("decode atlas: %v", err)
|
||||
}
|
||||
if len(a.Stages) == 0 {
|
||||
t.Fatal("atlas has no stages")
|
||||
}
|
||||
|
||||
// The generate:ci-jobs stage must be populated from the real cd.yml jobs.
|
||||
got := map[string]bool{}
|
||||
for _, s := range a.Stages {
|
||||
if s.No == "STAGE 06" {
|
||||
for _, n := range s.Nodes {
|
||||
got[n.Title] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"guard", "check", "build", "deploy"} {
|
||||
if !got[want] {
|
||||
t.Fatalf("CI stage missing generated job %q (got %v)", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownPath404(t *testing.T) {
|
||||
srv := httptest.NewServer(NewHandler())
|
||||
defer srv.Close()
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<body>
|
||||
<header>
|
||||
<h1><b>CAD</b> Atlas · From Signal to Pod</h1>
|
||||
<span class="sub mono">one human gate · everything up- and downstream is agents · <em>v0.3 static snapshot (→ live in Phase C)</em></span>
|
||||
<span class="sub mono">one human gate · everything up- and downstream is agents · <em>v0.4 · data-driven (CI stage generated from cd.yml)</em></span>
|
||||
<div class="controls">
|
||||
<button id="replay"><span class="dot"></span> Replay</button>
|
||||
<button id="slowmo">Slow-mo · <span id="slowState">off</span></button>
|
||||
@@ -145,20 +145,22 @@
|
||||
<footer class="mono">
|
||||
CAD → CI → CD · intent→specify→dispatch · build→test→validate · deploy→ship.
|
||||
Dashed violet = feedback bus (stage 08 → TELOS: deploy outcome scored vs originating goal).
|
||||
Data: static inventory from <code>brain</code> (2026-07-19). Phase C swaps these arrays for live reads of
|
||||
<code>assessor-loop</code> ledger · <code>session_log</code> · Gitea run API · Flux events.
|
||||
Data served from <code>/api/atlas.json</code> (authored <code>atlas.json</code> + CI stage generated from the live <code>cd.yml</code>).
|
||||
Phase C plugs in live reads of <code>assessor-loop</code> ledger · <code>session_log</code> · Gitea run API · Flux events.
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const SUBSTRATE=[
|
||||
// Inline data is an offline FALLBACK. Authoritative data is fetched from
|
||||
// /api/atlas.json (authored atlas.json + CI stage generated from the real cd.yml).
|
||||
let SUBSTRATE=[
|
||||
{n:"koala", k:"RTX 5070 · k3s control-plane · Gitea · LiteLLM :30401 · llama-swap :31234 · searxng"},
|
||||
{n:"iguana", k:"M2 Ultra · Ollama / mlx"},
|
||||
{n:"flamingo",k:"daily driver · ~/dev"},
|
||||
{n:"piguard",k:"NGINX reverse-proxy · ntfy"},
|
||||
];
|
||||
const NS="Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council";
|
||||
let NS="Tailscale mesh · ns: ai-stack · supervisor(→brain) · gitea-mcp · infra-mcp · council";
|
||||
|
||||
const STAGES=[
|
||||
let STAGES=[
|
||||
{no:"STAGE 00",cls:"",title:"Signals",path:"→ mathias/signals",nodes:[
|
||||
{t:"Applied AI Radar",d:"Daily Tier-1 + weekly Tier-2 deep pass. Verified-primary bar (paper/benchmark/code/named-lab).",tags:["cron · daily/weekly","→ signals #1–26+"]},
|
||||
{t:"Manual capture",d:"claude.ai strategic drop · brain capture tool.",tags:["ad-hoc"]},
|
||||
@@ -200,14 +202,17 @@ const STAGES=[
|
||||
]},
|
||||
];
|
||||
|
||||
const sub=document.getElementById('substrate');
|
||||
SUBSTRATE.forEach(h=>{const el=document.createElement('div');el.className='host';
|
||||
el.innerHTML=`<b>${h.n}</b><span class="k mono">${h.k}</span>`;sub.appendChild(el);});
|
||||
const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh);
|
||||
|
||||
const track=document.getElementById('track');
|
||||
const stageEls=[];
|
||||
STAGES.forEach(s=>{
|
||||
let stageEls=[];
|
||||
function renderAtlas(){
|
||||
const sub=document.getElementById('substrate');
|
||||
sub.querySelectorAll('.host').forEach(el=>el.remove());
|
||||
SUBSTRATE.forEach(h=>{const el=document.createElement('div');el.className='host';
|
||||
el.innerHTML=`<b>${h.n}</b><span class="k mono">${h.k}</span>`;sub.appendChild(el);});
|
||||
const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh);
|
||||
stageEls=[];
|
||||
track.querySelectorAll('.stage').forEach(el=>el.remove());
|
||||
STAGES.forEach(s=>{
|
||||
const st=document.createElement('div');st.className='stage '+s.cls;
|
||||
let h=`<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
|
||||
s.nodes.forEach(n=>{
|
||||
@@ -219,7 +224,8 @@ STAGES.forEach(s=>{
|
||||
h+=`<div class="node ${n.cls||''}">${inner}</div>`;
|
||||
});
|
||||
st.innerHTML=h;track.appendChild(st);stageEls.push(st);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- geometry ---- */
|
||||
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
|
||||
@@ -265,8 +271,19 @@ document.getElementById('replay').onclick=replay;
|
||||
document.getElementById('slowmo').onclick=e=>{slow=!slow;e.currentTarget.classList.toggle('on',slow);
|
||||
document.getElementById('slowState').textContent=slow?'on':'off';replay();};
|
||||
window.addEventListener('resize',()=>{clearTimeout(window._r);window._r=setTimeout(replay,150);});
|
||||
window.addEventListener('load',replay);
|
||||
build();
|
||||
async function init(){
|
||||
try{
|
||||
const r=await fetch('/api/atlas.json');
|
||||
if(!r.ok) throw new Error('atlas.json '+r.status);
|
||||
const data=await r.json();
|
||||
if(Array.isArray(data.substrate)) SUBSTRATE=data.substrate;
|
||||
if(typeof data.ns==='string') NS=data.ns;
|
||||
if(Array.isArray(data.stages)) STAGES=data.stages;
|
||||
}catch(e){ console.warn('atlas: using inline fallback —',e); }
|
||||
renderAtlas();
|
||||
replay();
|
||||
}
|
||||
window.addEventListener('load',init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user