From e14d9ae8e3f7498ce8669f0c698ff4ce87f0fc44 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Fri, 22 May 2026 22:32:01 +0000 Subject: [PATCH] feat(galleryop): add NodeProgress + OpStatus.Nodes for per-node breakdown Adds the data model the UI needs to render an expandable per-node breakdown of a fanned-out backend install. NodeProgress carries node identity (ID + name), per-node status (queued / running_on_worker / success / error / downloading), the current file + bytes + percentage from the Phase 2 progress stream, and any per-node error. OpStatus.Nodes is the slice the /api/operations handler will surface in a follow-up. Signed-off-by: Ettore Di Giacinto --- core/services/galleryop/node_progress_test.go | 42 +++++++++++++++++++ core/services/galleryop/operation.go | 24 +++++++++++ 2 files changed, 66 insertions(+) create mode 100644 core/services/galleryop/node_progress_test.go diff --git a/core/services/galleryop/node_progress_test.go b/core/services/galleryop/node_progress_test.go new file mode 100644 index 000000000..e2478c5e2 --- /dev/null +++ b/core/services/galleryop/node_progress_test.go @@ -0,0 +1,42 @@ +package galleryop_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mudler/LocalAI/core/services/galleryop" +) + +var _ = Describe("OpStatus.Nodes", func() { + It("defaults to empty on a fresh OpStatus", func() { + os := &galleryop.OpStatus{} + Expect(os.Nodes).To(BeEmpty()) + }) + + It("JSON round-trips with all NodeProgress fields", func() { + os := &galleryop.OpStatus{ + Nodes: []galleryop.NodeProgress{ + { + NodeID: "node-1", + NodeName: "worker-a", + Status: "running_on_worker", + FileName: "vllm.tar.zst", + Current: "412 MB", + Total: "2.1 GB", + Percentage: 19.6, + Phase: "downloading", + Error: "", + }, + }, + } + raw, err := json.Marshal(os) + Expect(err).ToNot(HaveOccurred()) + + got := &galleryop.OpStatus{} + Expect(json.Unmarshal(raw, got)).To(Succeed()) + Expect(got.Nodes).To(HaveLen(1)) + Expect(got.Nodes[0]).To(Equal(os.Nodes[0])) + }) +}) diff --git a/core/services/galleryop/operation.go b/core/services/galleryop/operation.go index c022a8976..b9e16c8e6 100644 --- a/core/services/galleryop/operation.go +++ b/core/services/galleryop/operation.go @@ -53,6 +53,30 @@ type OpStatus struct { GalleryElementName string `json:"gallery_element_name"` Cancelled bool `json:"cancelled"` // Cancelled is true if the operation was cancelled Cancellable bool `json:"cancellable"` // Cancellable is true if the operation can be cancelled + + // Nodes is the per-node breakdown for a fanned-out backend install. + // Populated by DistributedBackendManager (per-node terminal status) + // and by the Phase 2 progress bridge (per-byte ticks). The + // /api/operations handler surfaces this so the UI can render an + // expandable per-node view of an in-flight install. + Nodes []NodeProgress `json:"nodes,omitempty"` +} + +// NodeProgress is a single node's contribution to a backend install +// operation. Populated by DistributedBackendManager (per-node terminal +// status) and by the Phase 2 progress bridge (per-byte ticks). Read by +// the /api/operations handler so the UI can render an expandable +// per-node breakdown. +type NodeProgress struct { + NodeID string `json:"node_id"` + NodeName string `json:"node_name"` + Status string `json:"status"` // "queued" | "running_on_worker" | "success" | "error" | "downloading" + FileName string `json:"file_name,omitempty"` + Current string `json:"current,omitempty"` + Total string `json:"total,omitempty"` + Percentage float64 `json:"percentage"` + Phase string `json:"phase,omitempty"` + Error string `json:"error,omitempty"` } type OpCache struct {