Adding "not running what the mesh would send it" to `status` and not to the board would have left two answers to one question with a person in front of each — which is the single thing this page's design forbids, introduced by the change that was supposed to make the question answerable. The published JSON carries it as well, so the page, the command and anything built against either say the same thing from the same read. Additive, because that shape is hard to change once anything is built against it. Never told stays separate from out of date on the page as it is everywhere else: same remedy, and nobody has ever asked that machine to be anything.
130 lines
4.6 KiB
Go
130 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-control/internal/inventory"
|
|
)
|
|
|
|
// The same answers, in a shape something other than a person can read.
|
|
//
|
|
// A board reads through interfaces and holds nothing (novox/hq 03-DESIGN/01-to-be/11-a-board.md).
|
|
// Everything it needs is already answered by these commands — as text, for people, which is not
|
|
// something a page can read. So each of them can say it again as JSON.
|
|
//
|
|
// **`--json` rather than a serving API**, because nothing needs one yet: whatever serves a board
|
|
// runs the command, and the constraint in the design holds either way — the board never touches a
|
|
// context's store. An API is the larger thing and should wait until something is asking for it.
|
|
//
|
|
// **These shapes are hard to change once anything is built against them.** So they stay close to
|
|
// what the domain already calls things, and carry no summary field that would have to be kept
|
|
// true. Nothing here is derived that a reader could not derive.
|
|
|
|
// meshStatus is what `status --json` says: the three questions, in the order they are asked.
|
|
type meshStatus struct {
|
|
// Wrong is every machine whose last declaration was refused or partly failed.
|
|
Wrong []machineDoing `json:"wrong"`
|
|
// Quiet is every machine not heard from lately. Not the same as wrong: new, switched off and
|
|
// unreachable are not "tried and could not".
|
|
Quiet []machineQuiet `json:"quiet"`
|
|
// Behind is every module built from something older than its source has.
|
|
Behind []moduleBehind `json:"behind"`
|
|
// Waiting is every machine not running what the mesh would send it. The same question as
|
|
// Behind one level down: that says the catalogue is old, this says a machine is — and only
|
|
// this one has somebody's change waiting inside it.
|
|
Waiting []machineWaiting `json:"waiting"`
|
|
// Machines is how many the mesh knows about, so a reader can tell "none wrong" from
|
|
// "none at all".
|
|
Machines int `json:"machines"`
|
|
}
|
|
|
|
type machineDoing struct {
|
|
Node string `json:"node"`
|
|
// Outcome is refused or failed. Kept distinct all the way out: they are fixed in different
|
|
// places, and one word for both sends half the readers to the wrong one.
|
|
Outcome string `json:"outcome"`
|
|
Refused string `json:"refused,omitempty"`
|
|
Failed []struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
} `json:"failed,omitempty"`
|
|
Applied int `json:"applied"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
type machineWaiting struct {
|
|
Node string `json:"node"`
|
|
// Never is true when nothing has ever been sent to it. Not out of date: nobody has ever asked
|
|
// this machine to be anything, and the two read differently to whoever is looking.
|
|
Never bool `json:"never"`
|
|
Sent *time.Time `json:"sent,omitempty"`
|
|
}
|
|
|
|
type machineQuiet struct {
|
|
Node string `json:"node"`
|
|
// LastSeen is absent when the machine has never spoken, which is a different thing from
|
|
// having been quiet for a while.
|
|
LastSeen *time.Time `json:"lastSeen,omitempty"`
|
|
}
|
|
|
|
type moduleBehind struct {
|
|
Module string `json:"module"`
|
|
BuiltFrom string `json:"builtFrom"`
|
|
Head string `json:"head"`
|
|
On []string `json:"on"`
|
|
}
|
|
|
|
// statusAsJSON answers the same three questions as the text form, from the same calls.
|
|
func statusAsJSON(wrong []inventory.Doing, nodes []inventory.Node, quiet []inventory.Node,
|
|
behind map[string][]string, sources map[string]inventory.Source,
|
|
waiting []inventory.Machine) ([]byte, error) {
|
|
|
|
out := meshStatus{Machines: len(nodes), Wrong: []machineDoing{},
|
|
Quiet: []machineQuiet{}, Behind: []moduleBehind{}, Waiting: []machineWaiting{}}
|
|
|
|
for _, m := range waiting {
|
|
out.Waiting = append(out.Waiting, machineWaiting{
|
|
Node: m.Node, Never: m.Never, Sent: m.SentAt})
|
|
}
|
|
|
|
for _, d := range wrong {
|
|
row := machineDoing{
|
|
Node: d.Node, Outcome: d.Outcome, Refused: d.Refused, Applied: d.Applied, At: d.At,
|
|
}
|
|
for _, f := range d.Failed {
|
|
row.Failed = append(row.Failed, struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
}{ID: f.ID, Error: f.Error})
|
|
}
|
|
out.Wrong = append(out.Wrong, row)
|
|
}
|
|
for _, n := range quiet {
|
|
row := machineQuiet{Node: n.Name}
|
|
if !n.LastSeen.IsZero() {
|
|
seen := n.LastSeen
|
|
row.LastSeen = &seen
|
|
}
|
|
out.Quiet = append(out.Quiet, row)
|
|
}
|
|
for module, on := range behind {
|
|
from := sources[module]
|
|
out.Behind = append(out.Behind, moduleBehind{
|
|
Module: module, BuiltFrom: from.BuiltFrom, Head: from.Head, On: on,
|
|
})
|
|
}
|
|
return json.MarshalIndent(out, "", " ")
|
|
}
|
|
|
|
// say prints a value as JSON, for the commands that can answer either way.
|
|
func say(value any) error {
|
|
body, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
return nil
|
|
}
|