From ab5f76737203d9f80c18c4b240663bdf0d4c0e66 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:30:23 +0100 Subject: [PATCH] CAT-P2: the overview page: a handler that answers GET only, on a port for this machine only, through a required route The module's bundle (the one process with the bus) serves the page on the port its manifest declares under listens, from: machine, and requires a route that reaches it. No form, no script, no verb behind it; any method but GET is 405. A source that does not answer is said on the page, never shown as an empty list. Issue 1968. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../cmd/mesh-knowledge/bus_test.go | 2 +- .../mesh-knowledge/cmd/mesh-knowledge/kb.go | 3 + .../mesh-knowledge/cmd/mesh-knowledge/main.go | 1 + .../mesh-knowledge/cmd/mesh-knowledge/page.go | 139 ++++++++++++++++++ .../cmd/mesh-knowledge/page_test.go | 69 +++++++++ modules/mesh-knowledge/module.json | 17 ++- 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/page.go create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/page_test.go diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go index 03c87498e..48ce0a357 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go @@ -155,7 +155,7 @@ func (w *busWorld) start(t *testing.T) (*bundle, string) { _ = os.WriteFile(settings, []byte(`{"judge-model": "sonnet", "drafting": "off"}`), 0o600) status := filepath.Join(dir, "health.json") cmd := exec.Command(os.Args[0], "-test.run=^$") - cmd.Env = append(os.Environ(), bundleVar+"=1", "MESH_KNOWLEDGE_SETTINGS="+settings, "MESH_KNOWLEDGE_STATUS="+status, + cmd.Env = append(os.Environ(), bundleVar+"=1", "MESH_KNOWLEDGE_PAGE=off", "MESH_KNOWLEDGE_SETTINGS="+settings, "MESH_KNOWLEDGE_STATUS="+status, "MESH_SERVED_MODULE="+Module, "MESH_MODULE="+Module, "MESH_NODE=test") in, _ := cmd.StdinPipe() out, _ := cmd.StdoutPipe() diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go b/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go index 80d135422..dc22d30a2 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/kb.go @@ -102,6 +102,9 @@ type KB struct { Now func() time.Time Logf func(format string, a ...any) + // Overview is the overview page's model, when the page runs. + Overview *Overview + // calls are the tool calls in flight, by the proven caller of each (noteCall). calls sync.Map diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go index 818e9b0cb..9c0940b16 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/main.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/main.go @@ -106,6 +106,7 @@ func serve(k *KB, status string) { k.kick = make(chan struct{}, 1) go hear(k) go background(k) + go ServePage(k) if err := stdio.Serve("", append(append(Tools(k, callerOf), healthTool(status, k.now, k), highestTool(k), marksTool(k)), moduleTools(k, callerOf)...)); err != nil { logf("%v", err) os.Exit(1) diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/page.go b/modules/mesh-knowledge/cmd/mesh-knowledge/page.go new file mode 100644 index 000000000..37e52883a --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/page.go @@ -0,0 +1,139 @@ +package main + +// The overview page's handler and the process that keeps it (novox/hq to-be 55 §11, ADR 0616 §4): GET only, no form and no +// verb behind it, served by the module's bundle (the one process with the bus: the ledger and the sources' verbs are reached +// through it) on a port the manifest declares for this machine only, reached through the route proxy by a route the module requires. + +import ( + "context" + "fmt" + "html/template" + "net/http" + "os" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// PageAddress is where the page listens unless MESH_KNOWLEDGE_PAGE says otherwise: the port the manifest declares under +// `listens`, which the node's packet filter opens for this machine only. +const PageAddress = ":8480" + +// PageHandler renders the model as a page; any method but GET is refused, nothing is written. +func PageHandler(o *Overview) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.Header().Set("Allow", http.MethodGet) + http.Error(w, "this page is read-only: GET only", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + m, err := o.Model() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") + if err := pageTemplate.Execute(w, m); err != nil { + fmt.Fprintf(os.Stderr, "[mesh-knowledge] the page could not be rendered: %v\n", err) + } + }) +} + +var pageFuncs = template.FuncMap{ + "clock": func(t time.Time) string { + if t.IsZero() { + return "never" + } + return t.Local().Format("2006-01-02 15:04") + }, + "join": strings.Join, +} + +var pageTemplate = template.Must(template.New("page").Funcs(pageFuncs).Parse(` + +The mesh at a glance + +

The mesh at a glance

+

Read at {{clock .At}}. Sources: +{{range .Sources}}{{.Name}}: {{if .Answered}}answered {{clock .Last}}{{else}}does not answer{{if .Mark}} ({{.Mark.Cause}}: {{.Mark.Why}}; last answer {{clock .Last}}){{end}}{{end}}{{end}}

+ +

Designs in progress

+{{range .Sources}}{{if and (eq .Name "issue-tracker") (not .Answered)}}

The issue tracker does not answer: this block is not empty, it is unknown (last answer {{clock .Last}}).

{{end}}{{end}} +{{if .Designs}} +{{else}}

No design has a milestone open.

{{end}} + +

Records

+

Accepted in the last seven days

+{{if .Accepted}}{{else}}

None.

{{end}} +

Waiting

+{{if .Waiting}}{{else}}

None.

{{end}} + +

Waiting on you

+{{range .Sources}}{{if and (eq .Name "operator-channel") (not .Answered)}}

The operator channel does not answer: the asks below are not complete (last answer {{clock .Last}}).

{{end}}{{if and (eq .Name "mesh-delivery") (not .Answered)}}

Delivery does not answer: the groups below are not complete (last answer {{clock .Last}}).

{{end}}{{end}} +

Records waiting for your Approve

+{{if .Operator.Records}}{{else}}

None.

{{end}} +

Open asks

+{{if .Operator.Asks}}{{else}}

None.

{{end}} +

Delivery groups waiting

+{{if .Operator.Groups}}{{else}}

None.

{{end}} +`)) + +// ServePage starts the overview: the model's first view, the events kept, the unanswered sources asked again on their backoff, +// and the page on its address. It runs for the life of the process. +func ServePage(k *KB) { + o := NewOverview(k.Seats, func() ([]*Entry, error) { return k.Store.All(context.Background()) }, nil) + k.Overview = o + <-stdio.Served() + for source, events := range OverviewEvents { + for _, pattern := range events { + source, pattern := source, pattern + go func() { + for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { + err := stdio.Subscribe(pattern, func(e stdio.Envelope) error { + o.Heard(source, pattern, e.Body) + return nil + }) + if err == nil { + return + } + logf("[mesh-knowledge] the page does not hear %s yet (%v); asking again in %s", pattern, err, wait) + time.Sleep(wait) + } + }() + } + } + o.Start() + go func() { + for range time.Tick(5 * time.Second) { + o.Silent() + o.Due() + } + }() + addr := os.Getenv("MESH_KNOWLEDGE_PAGE") + if addr == "" { + addr = PageAddress + } + if addr == "off" { + return // a test's bundle serves no page + } + srv := &http.Server{Addr: addr, Handler: PageHandler(o), ReadHeaderTimeout: 10 * time.Second} + logf("[mesh-knowledge] the overview page listens on %s", addr) + if err := srv.ListenAndServe(); err != nil { + logf("[mesh-knowledge] the overview page stopped: %v", err) + } +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/page_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/page_test.go new file mode 100644 index 000000000..1cb57e8ef --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/page_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "slices" + "strconv" + "strings" + "testing" + "time" +) + +// The page's handler answers GET and nothing else; its manifest declares the port for this machine only and requires a route +// (ADR 0616 §4, AC P2). +func TestThePageAnswersGetOnlyAndTheManifestKeepsItToThisMachine(t *testing.T) { + w := newOverviewWorld(t) + w.entries = []*Entry{recordEntry(301, StatusAccepted, "2026-11-02", w.now.Add(-24*time.Hour), nil)} + w.o.Start() + h := PageHandler(w.o) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + body := rec.Body.String() + if rec.Code != 200 || !strings.Contains(body, "Design 47, milestone 1") || !strings.Contains(body, "ADR 0301") || !strings.Contains(body, "Approve ADR 0301") || + !strings.Contains(body, "fix/a") { + t.Fatalf("%d\n%.600s", rec.Code, body) + } + if strings.Contains(body, "