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"` // Reported is every machine's last word beside when it was last sent a declaration. A // machine whose report is newer than its send has acted on the current declaration; one // whose is older is still working — and Waiting cannot tell those apart, because the sent // digest is recorded at send, not at apply. Reported []machineReported `json:"reported"` // 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 machineReported struct { Node string `json:"node"` Outcome string `json:"outcome"` At *time.Time `json:"at,omitempty"` Sent *time.Time `json:"sent,omitempty"` // Current is whether the last report names the declaration last sent. Not derivable from // the timestamps beside it: an apply begun under the previous declaration reports after the // next send, newer and still about the old words. Current bool `json:"current"` } 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, reported []inventory.Reported) ([]byte, error) { out := meshStatus{Machines: len(nodes), Wrong: []machineDoing{}, Quiet: []machineQuiet{}, Behind: []moduleBehind{}, Waiting: []machineWaiting{}, Reported: []machineReported{}} for _, r := range reported { out.Reported = append(out.Reported, machineReported{ Node: r.Node, Outcome: r.Outcome, At: r.At, Sent: r.Sent, Current: r.Current}) } 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 }