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
This commit is contained in:
2026-11-04 12:30:23 +01:00
parent 40b1307839
commit ab5f767372
6 changed files with 229 additions and 2 deletions
@@ -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()
@@ -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
@@ -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)
@@ -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(`<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>The mesh at a glance</title>
<style>
:root{color-scheme:light dark;--bg:#fff;--fg:#1c1c1e;--mute:#6b6b70;--line:#d8d8dc;--warn:#9a5b00;--bad:#a3001d}
@media (prefers-color-scheme:dark){:root{--bg:#161618;--fg:#ececf0;--mute:#9a9aa2;--line:#35353a;--warn:#e0a040;--bad:#ff6b80}}
body{font:15px/1.5 system-ui,sans-serif;background:var(--bg);color:var(--fg);max-width:60rem;margin:2rem auto;padding:0 1rem}
h1{font-size:1.4rem}h2{font-size:1.1rem;margin-top:2rem;border-bottom:1px solid var(--line);padding-bottom:.25rem}
.mute{color:var(--mute)}.warn{color:var(--warn)}.bad{color:var(--bad)}li{margin:.25rem 0}ul{padding-left:1.2rem}
code{font-size:.9em}.src{display:inline-block;margin-right:1rem}
</style></head><body>
<h1>The mesh at a glance</h1>
<p class="mute">Read at {{clock .At}}. Sources:
{{range .Sources}}<span class="src">{{.Name}}: {{if .Answered}}answered {{clock .Last}}{{else}}<span class="bad">does not answer</span>{{if .Mark}} ({{.Mark.Cause}}: {{.Mark.Why}}; last answer {{clock .Last}}){{end}}{{end}}</span>{{end}}</p>
<h2>Designs in progress</h2>
{{range .Sources}}{{if and (eq .Name "issue-tracker") (not .Answered)}}<p class="bad">The issue tracker does not answer: this block is not empty, it is unknown (last answer {{clock .Last}}).</p>{{end}}{{end}}
{{if .Designs}}<ul>{{range .Designs}}<li><strong>Design {{.Design}}, milestone {{.ID}}</strong> ({{.Status}}): {{.Outcome}}
{{if .Issues}}<ul>{{range .Issues}}<li>issue {{.Number}}{{if .Status}} <span class="mute">[{{.Status}}]</span>{{end}} {{.Title}}</li>{{end}}</ul>{{end}}</li>{{end}}</ul>
{{else}}<p class="mute">No design has a milestone open.</p>{{end}}
<h2>Records</h2>
<h3>Accepted in the last seven days</h3>
{{if .Accepted}}<ul>{{range .Accepted}}<li>{{.Record}}: {{.Title}} <span class="mute">({{.Status}}, {{.Date}})</span></li>{{end}}</ul>{{else}}<p class="mute">None.</p>{{end}}
<h3>Waiting</h3>
{{if .Waiting}}<ul>{{range .Waiting}}<li>{{.Record}}: {{.Title}} <span class="mute">waits for {{.Waits}}</span></li>{{end}}</ul>{{else}}<p class="mute">None.</p>{{end}}
<h2>Waiting on you</h2>
{{range .Sources}}{{if and (eq .Name "operator-channel") (not .Answered)}}<p class="bad">The operator channel does not answer: the asks below are not complete (last answer {{clock .Last}}).</p>{{end}}{{if and (eq .Name "mesh-delivery") (not .Answered)}}<p class="bad">Delivery does not answer: the groups below are not complete (last answer {{clock .Last}}).</p>{{end}}{{end}}
<h3>Records waiting for your Approve</h3>
{{if .Operator.Records}}<ul>{{range .Operator.Records}}<li>{{.Record}}: {{.Title}}</li>{{end}}</ul>{{else}}<p class="mute">None.</p>{{end}}
<h3>Open asks</h3>
{{if .Operator.Asks}}<ul>{{range .Operator.Asks}}<li>{{.Headline}} <span class="mute">from {{.Asker}}{{if .Offered}}, offered on {{join .Offered ", "}}{{end}}</span></li>{{end}}</ul>{{else}}<p class="mute">None.</p>{{end}}
<h3>Delivery groups waiting</h3>
{{if .Operator.Groups}}<ul>{{range .Operator.Groups}}<li><code>{{.ID}}</code> {{.State}}{{if .Why}}: {{.Why}}{{end}}</li>{{end}}</ul>{{else}}<p class="mute">None.</p>{{end}}
</body></html>`))
// 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)
}
}
@@ -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, "<form") || strings.Contains(body, "<script") || !strings.Contains(rec.Header().Get("Content-Security-Policy"), "default-src 'none'") {
t.Fatalf("the page carries a form or a script, or no policy: %v", rec.Header())
}
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodHead, http.MethodOptions} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(method, "/", strings.NewReader("x")))
if rec.Code != http.StatusMethodNotAllowed || rec.Header().Get("Allow") != http.MethodGet {
t.Errorf("%s answered %d", method, rec.Code)
}
}
if rec := httptest.NewRecorder(); func() int { h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/other", nil)); return rec.Code }() != 404 {
t.Error("another path answered")
}
// A source that does not answer is said on the page, never shown as an empty list.
w.fail[AsksVerb] = true
w.o.Rebuild(SourceAsks)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if !strings.Contains(rec.Body.String(), "The operator channel does not answer") || !strings.Contains(rec.Body.String(), "verb: no answer in time") {
t.Fatalf("%.1500s", rec.Body.String())
}
// The manifest: the port listens from this machine, the route is required and the page is the one the code serves.
_, raw := readManifest(t)
var m struct {
Requires []any `json:"requires"`
Listens []struct {
Name string `json:"name"`
Port int `json:"port"`
From string `json:"from"`
} `json:"listens"`
Contributes map[string]map[string]string `json:"contributes"`
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
t.Fatal(err)
}
if len(m.Listens) != 1 || m.Listens[0].From != "machine" || ":"+strconv.Itoa(m.Listens[0].Port) != PageAddress || m.Contributes["route"]["endpoint"] != m.Listens[0].Name {
t.Fatalf("%+v", m)
}
if !slices.Contains(m.Requires, any("route")) {
t.Fatal("the route is not required")
}
}
+16 -1
View File
@@ -4,6 +4,7 @@
"slug": "knowledge",
"requires": [
"postgres-database",
"route",
{
"name": "records", "storage": "ledger", "template": "ledger", "options": {"max-mib": 1024},
"types": [
@@ -82,11 +83,25 @@
"contributes": {
"postgres-database": {
"name": "knowledge"
},
"route": {
"label": "knowledge",
"endpoint": "overview"
}
},
"binds": {
"postgres-database": "${dir:state}/database.json"
"postgres-database": "${dir:state}/database.json",
"route": "${dir:state}/route.json"
},
"listens": [
{
"name": "overview",
"port": 8480,
"protocol": "tcp",
"from": "machine",
"why": "the overview page, read-only: only a process on this machine reaches it, and the operator through the route proxy (novox/hq ADR 0616 §4)"
}
],
"secrets": {
"postgres-database": "${dir:state}/database.secret"
},