From 0be227bd7e2896e3bec7907489061fa638a5583d Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 10 Sep 2026 21:12:06 +0200 Subject: [PATCH] status: one machine that cannot be worked out no longer takes the answer from the rest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `status --json` emitted no JSON at all when a single node was unresolvable. A blocked node is not on the private network, and a mesh whose hub is that node has no hub — which came back through the reading as a refusal, so `status` printed nothing and `status --json` put multi-line prose on stderr and not one byte on stdout. A machine-readable interface that stops being machine-readable exactly when something is wrong is one nobody can build an alarm on. Why a machine cannot be worked out is read as data now, per machine, through the same whoResolves the private network is built from — so this and the network agree about who could not be resolved rather than deciding it twice. The private network failing to compute is kept as a note beside it instead of ending the read: it is almost always a consequence of those same refusals, and every question that does not depend on it is still answered. It reaches all three ways of saying it, from the one reading: the text form leads with it because a machine here is in none of the answers below, the JSON carries `unresolved` (always a list, never null) and `network`, and the page has a section of its own. That also closes a silent success. A machine that resolves to nothing has nothing computed for it, so there is nothing to compare it against and nothing it can be behind — it appeared in no answer at all, and `status` reported a mesh where nothing could be sent anywhere as "all doing what they were told". statusAsJSON takes the whole reading now rather than a growing argument list, which is what let an answer be added to the text form and forgotten here. The two are one function's output in two shapes and must not be able to differ about what was asked. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- cmd/mesh-control/board.go | 52 ++++++++++++- cmd/mesh-control/build.go | 9 +++ cmd/mesh-control/readable.go | 50 ++++++++++-- cmd/mesh-control/readable_test.go | 11 ++- cmd/mesh-control/status.go | 52 ++++++++++++- cmd/mesh-control/status_test.go | 121 ++++++++++++++++++++++++++++++ 6 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 cmd/mesh-control/status_test.go diff --git a/cmd/mesh-control/board.go b/cmd/mesh-control/board.go index fc11d2a..49702ac 100644 --- a/cmd/mesh-control/board.go +++ b/cmd/mesh-control/board.go @@ -7,6 +7,8 @@ import ( "fmt" "html/template" "net/http" + "sort" + "strings" "time" ) @@ -94,8 +96,7 @@ func board() http.Handler { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - body, err := statusAsJSON(asked.wrong, asked.nodes, asked.quiet, asked.behind, asked.sources, - asked.waiting, asked.reported) + body, err := statusAsJSON(asked) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -129,7 +130,21 @@ type view struct { Quiet []quietMachine Behind []staleModule Waiting []waitingMachine - At string + // Unresolved is every machine that cannot be worked out at all. Shown above everything else, + // because a machine here is in none of the other lists: nothing was computed for it, so it is + // not broken, not quiet and not behind — and a page without this said "all well" about a mesh + // where nothing could be sent anywhere. + Unresolved []blockedMachine + // Network is why the private network could not be computed, when it could not. + Network string + At string +} + +type blockedMachine struct { + Node string + // Said is the mesh's own words, a line at a time. Every unmet requirement, not the first: + // a machine is usually blocked by more than one and fixing one of them changes nothing. + Said []string } type waitingMachine struct { @@ -165,7 +180,20 @@ type staleModule struct { } func viewOf(asked answers) view { - out := view{Machines: len(asked.nodes), At: time.Now().Format("15:04:05")} + out := view{Machines: len(asked.nodes), At: time.Now().Format("15:04:05"), + Network: asked.network} + var blocked []string + for name := range asked.refused { + blocked = append(blocked, name) + } + sort.Strings(blocked) + for _, name := range blocked { + one := blockedMachine{Node: name} + for _, line := range strings.Split(strings.TrimRight(asked.refused[name], "\n"), "\n") { + one.Said = append(one.Said, strings.TrimSpace(line)) + } + out.Unresolved = append(out.Unresolved, one) + } for _, d := range asked.wrong { one := brokenMachine{Node: d.Node, Outcome: d.Outcome, When: d.At.Local().Format("2006-01-02 15:04")} @@ -223,6 +251,22 @@ find out.

{{else}}

{{.Machines}} machine{{if ne .Machines 1}}s{{end}}

+{{if .Unresolved}} +

Can everything be worked out?

+ +

Nothing can be sent to a machine here, and it appears in none of the lists below: +nothing was computed for it, so there is nothing it can be behind.

+{{end}} +{{if .Network}} +

The private network could not be computed: {{.Network}}

+{{end}} +

Is anything broken?

{{if .Broken}}