From cad826461cb180b5d5cdbe58a82a941c5de0faf6 Mon Sep 17 00:00:00 2001 From: R0ck Date: Sat, 24 Oct 2026 16:55:03 +0100 Subject: [PATCH 1/2] firmwarelib: a build the catalogue has not answered for is checking, not unavailable On a fresh install the library called every pinned build unavailable until the catalogue fetch came back, which is exactly when Setup sends a new user there. Rows now say checking while the fetch runs, and say the catalogue could not be reached (with a retry) when it fails, rather than claiming the build is not published. The Actions column is wide enough for its labels. Fixes #772 --- .../app/session/firmwarelib/checking_test.go | 69 +++++++++++++++++++ internal/app/session/firmwarelib/domain.go | 3 + .../session/firmwarelib/firmwarecatalogue.go | 24 +++++-- .../app/session/firmwarelib/firmwarescan.go | 27 ++++++-- internal/app/session/libraryrows.go | 1 + internal/app/state/network.go | 7 ++ internal/ui/workbench/firmwarepanel.go | 2 +- internal/ui/workbench/firmwarerow.go | 20 +++++- internal/ui/workbench/firmwarewindow.go | 4 ++ 9 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 internal/app/session/firmwarelib/checking_test.go diff --git a/internal/app/session/firmwarelib/checking_test.go b/internal/app/session/firmwarelib/checking_test.go new file mode 100644 index 00000000..9962b1ed --- /dev/null +++ b/internal/app/session/firmwarelib/checking_test.go @@ -0,0 +1,69 @@ +package firmwarelib + +import ( + "context" + "testing" + + "github.com/MeshBench/meshbench/internal/app/session" + "github.com/MeshBench/meshbench/internal/app/state" + "github.com/MeshBench/meshbench/internal/world/scenario" +) + +// A build nobody has yet, while the catalogue has not answered, is "checking", +// not "not published". +// +// On a fresh install Setup sends a new user to the library to download a build +// for each role, and the library opened by calling every one of them +// unavailable: it had asked the catalogue and not yet heard back. +func TestTheLibrarySaysCheckingUntilTheCatalogueAnswers(t *testing.T) { + cache := t.TempDir() + t.Setenv("XDG_CACHE_HOME", cache) + t.Setenv("LOCALAPPDATA", cache) + + nodes := []scenario.Node{ + {Name: "hill", Kind: scenario.SimpleRepeater, Firmware: scenario.FirmwareRef{Version: "repeater-v1.17.1"}}, + } + store := state.New(10) + sim := &session.Sim{} + sim.BuildSeeded(nodes, 869.618, 1) + register(store, sim) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go store.Run(ctx) + + // A fetch in flight, held there so the test does not reach the network. + catalogueOf(sim).fetching = true + row := func() state.FirmwareRow { + t.Helper() + if _, err := store.Do(ctx, "firmware.library", nil); err != nil { + t.Fatal(err) + } + for _, r := range store.Snapshot().Library { + if r.Version == "repeater-v1.17.1" { + return r + } + } + t.Fatal("the pinned build has no row") + return state.FirmwareRow{} + } + + if r := row(); !r.Checking || r.Unavailable || r.CatalogueDown { + t.Errorf("while the catalogue is asked: %+v, want checking only", r) + } + + // The catalogue could not be reached: not known, rather than not there. + if _, err := store.Do(ctx, "firmware.published", publishedAnswer{failed: true}); err != nil { + t.Fatal(err) + } + if r := row(); !r.CatalogueDown || r.Unavailable || r.Checking { + t.Errorf("after a failed fetch: %+v, want catalogue down only", r) + } + + // Reached, and it is not there: now it is unavailable. + if _, err := store.Do(ctx, "firmware.published", []publishedBuild{}); err != nil { + t.Fatal(err) + } + if r := row(); !r.Unavailable || r.Checking || r.CatalogueDown { + t.Errorf("after the catalogue answered without it: %+v, want unavailable only", r) + } +} diff --git a/internal/app/session/firmwarelib/domain.go b/internal/app/session/firmwarelib/domain.go index 698fd5a6..d5620d65 100644 --- a/internal/app/session/firmwarelib/domain.go +++ b/internal/app/session/firmwarelib/domain.go @@ -41,6 +41,9 @@ type catalogue struct { // fetch has answered, empty after a fetch that failed. published []publishedBuild fetching bool + // failed is the last fetch not reaching the catalogue, so an empty + // published list means "could not ask" rather than "nothing is there". + failed bool } // catalogueOf is the one catalogue a Sim has, made on first use. diff --git a/internal/app/session/firmwarelib/firmwarecatalogue.go b/internal/app/session/firmwarelib/firmwarecatalogue.go index 8e6b3335..eed954db 100644 --- a/internal/app/session/firmwarelib/firmwarecatalogue.go +++ b/internal/app/session/firmwarelib/firmwarecatalogue.go @@ -74,13 +74,27 @@ func fillLibrary(s *session.Sim, w *state.World) { r := add(role, n.Firmware.Version, n.Firmware.Board) r.InUse++ } + cat := catalogueOf(s) out := make([]state.FirmwareRow, 0, len(rows)) for _, r := range rows { - // Nothing on disk and nothing published for this machine, yet nodes - // point at it: the scenario is asking for a build nobody here can - // run, and the row should say so rather than wait to be discovered - // at start time. - r.Unavailable = !r.OnDisk && !published[key(r.Role, r.Version, r.Board)] + if !r.OnDisk && !published[key(r.Role, r.Version, r.Board)] { + switch { + case cat.fetching || cat.published == nil: + // Not known yet. Said as unavailable, this is the first + // thing a new user saw: Setup sends them to the library to + // download a build for each role, and the library told them + // none was published, because it had not heard back yet. + r.Checking = true + case cat.failed: + r.CatalogueDown = true + default: + // Nothing on disk and nothing published for this machine, + // yet nodes point at it: the scenario is asking for a build + // nobody here can run, and the row should say so rather + // than wait to be discovered at start time. + r.Unavailable = true + } + } out = append(out, *r) } // In use first, then what is here, then the rest: the order somebody diff --git a/internal/app/session/firmwarelib/firmwarescan.go b/internal/app/session/firmwarelib/firmwarescan.go index cef9b7d1..7b0ecfb9 100644 --- a/internal/app/session/firmwarelib/firmwarescan.go +++ b/internal/app/session/firmwarelib/firmwarescan.go @@ -31,11 +31,21 @@ func registerFirmwareScan(st *state.Store, s *session.Sim) { }) st.HandleInternal("firmware.published", func(w *state.World, p any) (any, error) { - list, ok := p.([]publishedBuild) - if !ok { + var ans publishedAnswer + switch v := p.(type) { + case publishedAnswer: + ans = v + case []publishedBuild: + ans.builds = v + default: return nil, session.WrongCallback("firmware.published") } + list := ans.builds + if list == nil { + list = []publishedBuild{} + } catalogueOf(s).published = list + catalogueOf(s).failed = ans.failed catalogueOf(s).fetching = false // Rebuild the rows here, or the fetch lands in a field nobody reads // again: the panel asks for the library once, the network answers a @@ -64,7 +74,9 @@ func startPublishedFetch(s *session.Sim, st *state.Store) { // The native builds: MeshCore compiled for this machine, which is how // most nodes run. cat := &firmware.NativeCatalogue{CacheDir: firmware.DefaultCacheDir()} - if images, err := cat.List(ctx); err == nil { + images, err := cat.List(ctx) + failed := err != nil + if err == nil { for _, img := range images { if img.ForThisMachine() { list = append(list, publishedBuild{ @@ -81,6 +93,13 @@ func startPublishedFetch(s *session.Sim, st *state.Store) { list = append(list, publishedBoards(ctx)...) // An empty non-nil list on failure, so a dead network is one failed // fetch rather than a fetch per frame. - _, _ = st.Do(context.Background(), "firmware.published", list) + _, _ = st.Do(context.Background(), "firmware.published", publishedAnswer{builds: list, failed: failed}) }() } + +// publishedAnswer is what a fetch hands back: the builds, and whether the +// catalogue was reached at all. +type publishedAnswer struct { + builds []publishedBuild + failed bool +} diff --git a/internal/app/session/libraryrows.go b/internal/app/session/libraryrows.go index 25cc9548..3ad78efa 100644 --- a/internal/app/session/libraryrows.go +++ b/internal/app/session/libraryrows.go @@ -18,6 +18,7 @@ func libraryRows(rows []state.FirmwareRow) []map[string]any { // What a delete would break, and why a row nobody can honour is // not simply hidden. "in_use": b.InUse, "unavailable": b.Unavailable, + "checking": b.Checking, "catalogue_down": b.CatalogueDown, }) } return out diff --git a/internal/app/state/network.go b/internal/app/state/network.go index 57113260..994b853e 100644 --- a/internal/app/state/network.go +++ b/internal/app/state/network.go @@ -410,6 +410,13 @@ type FirmwareRow struct { // which reads as the library losing builds rather than as a pin nobody // can honour. Unavailable bool + // Checking marks a row whose build is not on disk while the catalogue of + // published builds has not answered yet. It may well be published; until + // the answer is in, nobody can say it is not. + Checking bool + // CatalogueDown marks a row whose build is not on disk when the catalogue + // could not be reached, so whether it is published is not known. + CatalogueDown bool // Native marks a build for this machine rather than an image for a board. // The two are not interchangeable and only one of them can be renamed. Native bool diff --git a/internal/ui/workbench/firmwarepanel.go b/internal/ui/workbench/firmwarepanel.go index b386b6b2..94240a55 100644 --- a/internal/ui/workbench/firmwarepanel.go +++ b/internal/ui/workbench/firmwarepanel.go @@ -76,7 +76,7 @@ var fwCols = []struct { width int }{ {"Role", 210}, {"Version", 250}, {"Runs as", 140}, {"Size", 70}, - {"On disk", 70}, {"Used by", 90}, {"Use for role", 150}, {"Actions", 70}, + {"On disk", 70}, {"Used by", 90}, {"Use for role", 150}, {"Actions", 96}, } func (p *firmwarePanel) Draw(t *theme.Theme, gtx layout.Context, s *state.Snapshot) layout.Dimensions { diff --git a/internal/ui/workbench/firmwarerow.go b/internal/ui/workbench/firmwarerow.go index fb766cd9..e657c435 100644 --- a/internal/ui/workbench/firmwarerow.go +++ b/internal/ui/workbench/firmwarerow.go @@ -37,8 +37,13 @@ func (p *firmwarePanel) row(t *theme.Theme, gtx layout.Context, s *state.Snapsho } if w.act.Clicked(gtx) { switch { - case r.Unavailable: - // Nothing to download and nothing to delete. + case r.CatalogueDown: + // Ask again: the answer was "could not reach it", not "no". + if p.OnAction != nil { + p.OnAction("firmware.rescan", nil) + } + case r.Unavailable || r.Checking: + // Nothing to download and nothing to delete - yet, for one of them. case !r.OnDisk: if p.OnAction != nil { p.OnAction("firmware.download", map[string]any{ @@ -81,8 +86,13 @@ func (p *firmwarePanel) row(t *theme.Theme, gtx layout.Context, s *state.Snapsho // nothing published this machine could fetch. Saying so here is the // difference between "the library lost a build" and "these nodes are // pinned to something that was never built for you". - if r.Unavailable { + switch { + case r.Unavailable: runsAs += " - not published for this machine" + case r.Checking: + runsAs += " - checking what is published" + case r.CatalogueDown: + runsAs += " - the catalogue could not be reached" } size := "-" if r.OnDisk { @@ -140,6 +150,10 @@ func (p *firmwarePanel) row(t *theme.Theme, gtx layout.Context, s *state.Snapsho // Nothing to fetch, so no button that implies // there is. label, ink = "unavailable", t.P.Warn + } else if r.Checking { + label, ink = "checking", t.P.Dim + } else if r.CatalogueDown { + label, ink = "retry", t.P.Accent } else if !r.OnDisk { label, ink = "get", t.P.Accent } else if p.confirm == key { diff --git a/internal/ui/workbench/firmwarewindow.go b/internal/ui/workbench/firmwarewindow.go index 2fc99ea4..94d92d0f 100644 --- a/internal/ui/workbench/firmwarewindow.go +++ b/internal/ui/workbench/firmwarewindow.go @@ -223,6 +223,10 @@ func (p *firmwareWindowPanel) stateChip(t *theme.Theme, gtx layout.Context, switch { case !r.OnDisk && r.Unavailable: label, ink = "not published for this machine", t.P.Warn + case !r.OnDisk && r.Checking: + label, ink = "checking what is published", t.P.Dim + case !r.OnDisk && r.CatalogueDown: + label, ink = "catalogue unreachable - refresh to ask again", t.P.Warn case !r.OnDisk: label, ink = "not downloaded", t.P.Dim case r.Native: From be1da422fa6e307295a6b5e1c02882c4896725f2 Mon Sep 17 00:00:00 2001 From: R0ck Date: Sat, 24 Oct 2026 19:22:18 +0100 Subject: [PATCH 2/2] firmwarelib: tell the two catalogues' failures apart A row for a board image said "unavailable" when the board catalogue could not be reached, because only the native catalogue's failure was kept. Each half now has its own, and the library rows' new fields are documented and in the Go and Python clients. --- CHANGELOG.md | 6 +++ docs/verb-reference.md | 2 +- docs/verbs.json | 2 +- .../app/session/firmwarelib/boardlist_test.go | 5 +- .../app/session/firmwarelib/checking_test.go | 47 ++++++++++++++----- internal/app/session/firmwarelib/domain.go | 8 ++-- .../session/firmwarelib/firmwarecatalogue.go | 8 ++-- .../firmwarelib/firmwarelib.verbs.json | 2 +- .../app/session/firmwarelib/firmwarescan.go | 22 +++++---- pkg/client-go/meshbench/types.go | 5 ++ pkg/client-python/meshbench/types.py | 5 ++ 11 files changed, 79 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc0e7ba..0b0b0da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,12 @@ had changed in them - which is the gap this file exists to close. columns were wider than the rail, so the header wrapped a letter to a line and left room for one row. A panel too narrow for the full row now lays rows out the way the Inspector does. +- **The firmware library says "checking" until the catalogue answers.** A + build not yet downloaded was marked unavailable while the catalogue was + still being asked, and for good if it could not be reached, so a first run + opened on a library of builds that looked unpublished. A row whose half of + the catalogue is unreachable now offers a retry. `firmware.library` rows + carry `checking` and `catalogue_down`. ## [0.3.0] - 2026-10-24 diff --git a/docs/verb-reference.md b/docs/verb-reference.md index 61532560..36e84e35 100644 --- a/docs/verb-reference.md +++ b/docs/verb-reference.md @@ -1863,7 +1863,7 @@ List every build there is, on disk and published together, so a build nobody has **Takes** nothing. -**Answers** `builds`, `count`. The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use` and `unavailable`. +**Answers** `builds`, `count`. The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use`, `unavailable`, `checking` and `catalogue_down`. A build that is not on disk is `checking` until the catalogue answers, `catalogue_down` if it could not be reached, and `unavailable` only once it answered without it. **Example** - list what could be run, fetched or not diff --git a/docs/verbs.json b/docs/verbs.json index 88a41bd9..bc16e7ab 100644 --- a/docs/verbs.json +++ b/docs/verbs.json @@ -2005,7 +2005,7 @@ "builds", "count" ], - "answers": "The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use` and `unavailable`.", + "answers": "The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use`, `unavailable`, `checking` and `catalogue_down`. A build that is not on disk is `checking` until the catalogue answers, `catalogue_down` if it could not be reached, and `unavailable` only once it answered without it.", "example": { "params": {}, "what": "list what could be run, fetched or not" diff --git a/internal/app/session/firmwarelib/boardlist_test.go b/internal/app/session/firmwarelib/boardlist_test.go index 06242812..cbd0eebb 100644 --- a/internal/app/session/firmwarelib/boardlist_test.go +++ b/internal/app/session/firmwarelib/boardlist_test.go @@ -33,7 +33,10 @@ func TestPublishedBoardsReachTheLibrary(t *testing.T) { t.Skip("the release catalogue came back empty, which is not this code's doing") } - got := publishedBoards(ctx) + got, err := publishedBoards(ctx) + if err != nil { + t.Skipf("the release catalogue was not reachable the second time: %v", err) + } if len(got) == 0 { t.Fatalf("the catalogue listed %d images and none of them reached the "+ "library: the emulated half is missing again", len(imgs)) diff --git a/internal/app/session/firmwarelib/checking_test.go b/internal/app/session/firmwarelib/checking_test.go index 9962b1ed..a6f48144 100644 --- a/internal/app/session/firmwarelib/checking_test.go +++ b/internal/app/session/firmwarelib/checking_test.go @@ -22,6 +22,8 @@ func TestTheLibrarySaysCheckingUntilTheCatalogueAnswers(t *testing.T) { nodes := []scenario.Node{ {Name: "hill", Kind: scenario.SimpleRepeater, Firmware: scenario.FirmwareRef{Version: "repeater-v1.17.1"}}, + {Name: "deck", Kind: scenario.SimpleRepeater, Firmware: scenario.FirmwareRef{ + Version: "v1.17.1", Board: "LilyGo_TDeck"}}, } store := state.New(10) sim := &session.Sim{} @@ -33,37 +35,58 @@ func TestTheLibrarySaysCheckingUntilTheCatalogueAnswers(t *testing.T) { // A fetch in flight, held there so the test does not reach the network. catalogueOf(sim).fetching = true - row := func() state.FirmwareRow { + row := func(version string) state.FirmwareRow { t.Helper() if _, err := store.Do(ctx, "firmware.library", nil); err != nil { t.Fatal(err) } for _, r := range store.Snapshot().Library { - if r.Version == "repeater-v1.17.1" { + if r.Version == version { return r } } - t.Fatal("the pinned build has no row") + t.Fatalf("the pinned build %s has no row", version) return state.FirmwareRow{} } + native, board := func() state.FirmwareRow { return row("repeater-v1.17.1") }, + func() state.FirmwareRow { return row("v1.17.1") } - if r := row(); !r.Checking || r.Unavailable || r.CatalogueDown { - t.Errorf("while the catalogue is asked: %+v, want checking only", r) + for name, r := range map[string]state.FirmwareRow{"native": native(), "board": board()} { + if !r.Checking || r.Unavailable || r.CatalogueDown { + t.Errorf("%s, while the catalogue is asked: %+v, want checking only", name, r) + } + } + + // The native half could not be reached: not known, rather than not + // there. The board half answered, and the image is not in it. + if _, err := store.Do(ctx, "firmware.published", publishedAnswer{nativeDown: true}); err != nil { + t.Fatal(err) + } + if r := native(); !r.CatalogueDown || r.Unavailable || r.Checking { + t.Errorf("native, after its catalogue failed: %+v, want catalogue down only", r) + } + if r := board(); !r.Unavailable || r.CatalogueDown || r.Checking { + t.Errorf("board, after its catalogue answered: %+v, want unavailable only", r) } - // The catalogue could not be reached: not known, rather than not there. - if _, err := store.Do(ctx, "firmware.published", publishedAnswer{failed: true}); err != nil { + // And the other way about. + if _, err := store.Do(ctx, "firmware.published", publishedAnswer{boardsDown: true}); err != nil { t.Fatal(err) } - if r := row(); !r.CatalogueDown || r.Unavailable || r.Checking { - t.Errorf("after a failed fetch: %+v, want catalogue down only", r) + if r := native(); !r.Unavailable || r.CatalogueDown || r.Checking { + t.Errorf("native, after its catalogue answered: %+v, want unavailable only", r) + } + if r := board(); !r.CatalogueDown || r.Unavailable || r.Checking { + t.Errorf("board, after its catalogue failed: %+v, want catalogue down only", r) } - // Reached, and it is not there: now it is unavailable. + // Both reached, neither has it: now both are unavailable. if _, err := store.Do(ctx, "firmware.published", []publishedBuild{}); err != nil { t.Fatal(err) } - if r := row(); !r.Unavailable || r.Checking || r.CatalogueDown { - t.Errorf("after the catalogue answered without it: %+v, want unavailable only", r) + for name, r := range map[string]state.FirmwareRow{"native": native(), "board": board()} { + if !r.Unavailable || r.Checking || r.CatalogueDown { + t.Errorf("%s, after the catalogue answered without it: %+v, want unavailable only", name, r) + } } } diff --git a/internal/app/session/firmwarelib/domain.go b/internal/app/session/firmwarelib/domain.go index d5620d65..fe9afdac 100644 --- a/internal/app/session/firmwarelib/domain.go +++ b/internal/app/session/firmwarelib/domain.go @@ -41,9 +41,11 @@ type catalogue struct { // fetch has answered, empty after a fetch that failed. published []publishedBuild fetching bool - // failed is the last fetch not reaching the catalogue, so an empty - // published list means "could not ask" rather than "nothing is there". - failed bool + // nativeDown and boardsDown are the last fetch not reaching that half of + // the catalogue, so a build missing from it means "could not ask" rather + // than "nothing is there". Two, because the halves are served from + // different places and one can answer while the other does not. + nativeDown, boardsDown bool } // catalogueOf is the one catalogue a Sim has, made on first use. diff --git a/internal/app/session/firmwarelib/firmwarecatalogue.go b/internal/app/session/firmwarelib/firmwarecatalogue.go index eed954db..27c6974d 100644 --- a/internal/app/session/firmwarelib/firmwarecatalogue.go +++ b/internal/app/session/firmwarelib/firmwarecatalogue.go @@ -85,7 +85,7 @@ func fillLibrary(s *session.Sim, w *state.World) { // download a build for each role, and the library told them // none was published, because it had not heard back yet. r.Checking = true - case cat.failed: + case r.Board == "" && cat.nativeDown, r.Board != "" && cat.boardsDown: r.CatalogueDown = true default: // Nothing on disk and nothing published for this machine, @@ -126,11 +126,11 @@ func fillLibrary(s *session.Sim, w *state.World) { // library, so this keeps the boards MeshBench can actually run and, for each // board and role, the newest version. Older ones stay one click away in the // firmware picker, which reads the catalogue directly. -func publishedBoards(ctx context.Context) []publishedBuild { +func publishedBoards(ctx context.Context) ([]publishedBuild, error) { cat := &emulated.BoardCatalogue{CacheDir: firmware.DefaultCacheDir()} imgs, err := cat.ListAll(ctx) if err != nil { - return nil + return nil, err } known := map[string]bool{} for _, b := range hw.Boards() { @@ -171,7 +171,7 @@ func publishedBoards(ctx context.Context) []publishedBuild { } return out[i].role < out[j].role }) - return out + return out, nil } // newerVersion compares vX.Y.Z by number, so v1.9.0 sorts below v1.17.0 - diff --git a/internal/app/session/firmwarelib/firmwarelib.verbs.json b/internal/app/session/firmwarelib/firmwarelib.verbs.json index 1d7cbaf2..b001b324 100644 --- a/internal/app/session/firmwarelib/firmwarelib.verbs.json +++ b/internal/app/session/firmwarelib/firmwarelib.verbs.json @@ -18,7 +18,7 @@ "builds", "count" ], - "answers": "The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use` and `unavailable`.", + "answers": "The catalogue is asked over the network in the background and its answer lands seconds later, so the first call in a session answers from disk alone and is worth making again. Each row in `builds` carries `role`, `version`, `board`, `bytes`, `on_disk`, `path`, `in_use`, `unavailable`, `checking` and `catalogue_down`. A build that is not on disk is `checking` until the catalogue answers, `catalogue_down` if it could not be reached, and `unavailable` only once it answered without it.", "example": { "params": {}, "what": "list what could be run, fetched or not" diff --git a/internal/app/session/firmwarelib/firmwarescan.go b/internal/app/session/firmwarelib/firmwarescan.go index 7b0ecfb9..c7c6ab26 100644 --- a/internal/app/session/firmwarelib/firmwarescan.go +++ b/internal/app/session/firmwarelib/firmwarescan.go @@ -45,7 +45,7 @@ func registerFirmwareScan(st *state.Store, s *session.Sim) { list = []publishedBuild{} } catalogueOf(s).published = list - catalogueOf(s).failed = ans.failed + catalogueOf(s).nativeDown, catalogueOf(s).boardsDown = ans.nativeDown, ans.boardsDown catalogueOf(s).fetching = false // Rebuild the rows here, or the fetch lands in a field nobody reads // again: the panel asks for the library once, the network answers a @@ -74,9 +74,8 @@ func startPublishedFetch(s *session.Sim, st *state.Store) { // The native builds: MeshCore compiled for this machine, which is how // most nodes run. cat := &firmware.NativeCatalogue{CacheDir: firmware.DefaultCacheDir()} - images, err := cat.List(ctx) - failed := err != nil - if err == nil { + images, nativeErr := cat.List(ctx) + if nativeErr == nil { for _, img := range images { if img.ForThisMachine() { list = append(list, publishedBuild{ @@ -90,16 +89,19 @@ func startPublishedFetch(s *session.Sim, st *state.Store) { // never did, so the whole emulated half of the library was missing: on // Linux the native builds hid it, and on a Mac, where no native build // exists yet, the panel was simply empty. - list = append(list, publishedBoards(ctx)...) + boards, boardsErr := publishedBoards(ctx) + list = append(list, boards...) // An empty non-nil list on failure, so a dead network is one failed // fetch rather than a fetch per frame. - _, _ = st.Do(context.Background(), "firmware.published", publishedAnswer{builds: list, failed: failed}) + _, _ = st.Do(context.Background(), "firmware.published", publishedAnswer{ + builds: list, nativeDown: nativeErr != nil, boardsDown: boardsErr != nil, + }) }() } -// publishedAnswer is what a fetch hands back: the builds, and whether the -// catalogue was reached at all. +// publishedAnswer is what a fetch hands back: the builds, and which half of +// the catalogue could not be reached. type publishedAnswer struct { - builds []publishedBuild - failed bool + builds []publishedBuild + nativeDown, boardsDown bool } diff --git a/pkg/client-go/meshbench/types.go b/pkg/client-go/meshbench/types.go index 52b3564e..9687ee17 100644 --- a/pkg/client-go/meshbench/types.go +++ b/pkg/client-go/meshbench/types.go @@ -145,6 +145,11 @@ type Build struct { // fails at start, which reads as the library losing builds rather than as // a pin nobody can honour. Unavailable bool `json:"unavailable"` + // Checking marks a build not on disk while the catalogue has not answered + // yet, and CatalogueDown one whose catalogue could not be reached: in both + // it may well be published, and nobody can yet say it is not. + Checking bool `json:"checking"` + CatalogueDown bool `json:"catalogue_down"` } // BuildDetails is one build in full: what a library row cannot hold. diff --git a/pkg/client-python/meshbench/types.py b/pkg/client-python/meshbench/types.py index d33b2bf7..83771f45 100644 --- a/pkg/client-python/meshbench/types.py +++ b/pkg/client-python/meshbench/types.py @@ -217,6 +217,11 @@ class Build: #: published. Pinning to one succeeds and then fails at start, which reads #: as the library losing builds rather than as a pin nobody can honour. unavailable: bool = False + #: Not on disk while the catalogue has not answered yet (``checking``), + #: or could not be reached (``catalogue_down``): in both it may well be + #: published, and nobody can yet say it is not. + checking: bool = False + catalogue_down: bool = False def __str__(self) -> str: if not self.board: