From 46e4f4c2079014c1ee3a796e6518cc0c3e871788 Mon Sep 17 00:00:00 2001 From: Luca Rinaldi Date: Wed, 17 Dec 2025 10:56:10 +0100 Subject: [PATCH 1/2] fix(update): always try to restart the app-cli daemon (#161) * fix(update): always try to restart the app-cli daemon * Update internal/update/apt/service.go Co-authored-by: Alby <30591904+Xayton@users.noreply.github.com> --------- Co-authored-by: Alby <30591904+Xayton@users.noreply.github.com> --- internal/update/apt/service.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/update/apt/service.go b/internal/update/apt/service.go index 860be68f..3c00c570 100644 --- a/internal/update/apt/service.go +++ b/internal/update/apt/service.go @@ -83,6 +83,18 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u defer s.lock.Unlock() defer close(eventsCh) + // At the end of the upgrade, always try to restart the services (that need it). + // This makes sure key services are restarted even if an error happens in the upgrade steps (for examples container images download). + defer func() { + eventsCh <- update.NewDataEvent(update.RestartEvent, "Upgrade completed. Restarting ...") + + err := restartServices(ctx) + if err != nil { + eventsCh <- update.NewErrorEvent(fmt.Errorf("error restarting services after upgrade: %w", err)) + return + } + }() + eventsCh <- update.NewDataEvent(update.StartEvent, "Upgrade is starting") stream := runUpgradeCommand(ctx, names) for line, err := range stream { @@ -127,13 +139,6 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u } eventsCh <- update.NewDataEvent(update.UpgradeLineEvent, line) } - eventsCh <- update.NewDataEvent(update.RestartEvent, "Upgrade completed. Restarting ...") - - err := restartServices(ctx) - if err != nil { - eventsCh <- update.NewErrorEvent(fmt.Errorf("error restarting services after upgrade: %w", err)) - return - } }() return eventsCh, nil From 19c9d6292e62124d212234d9a7b2ff3f7fc0d082 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 17 Dec 2025 15:54:44 +0100 Subject: [PATCH 2/2] Fix: get `/apps/:id/bricks` must return the same response as `/apps/:id/bricks/:id` (#167) * Unify BrickInstance types and add getSelectedModelOrDefault helper --- internal/api/docs/openapi.yaml | 30 +------------- internal/orchestrator/bricks/bricks.go | 23 ++++++----- internal/orchestrator/bricks/bricks_test.go | 44 ++++++++++++++++++++- internal/orchestrator/bricks/types.go | 14 +------ 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/internal/api/docs/openapi.yaml b/internal/api/docs/openapi.yaml index 2b34f1a9..c0017e88 100644 --- a/internal/api/docs/openapi.yaml +++ b/internal/api/docs/openapi.yaml @@ -1194,7 +1194,7 @@ components: properties: bricks: items: - $ref: '#/components/schemas/BrickInstanceListItem' + $ref: '#/components/schemas/BrickInstance' nullable: true type: array type: object @@ -1355,7 +1355,6 @@ components: $ref: '#/components/schemas/BrickVariable' description: 'Deprecated: use config_variables instead. This field is kept for backward compatibility.' - nullable: true type: object type: object BrickInstance: @@ -1390,33 +1389,6 @@ components: for backward compatibility.' type: object type: object - BrickInstanceListItem: - properties: - author: - type: string - category: - type: string - config_variables: - items: - $ref: '#/components/schemas/BrickConfigVariable' - type: array - id: - type: string - model: - type: string - name: - type: string - require_model: - type: boolean - status: - type: string - variables: - additionalProperties: - type: string - description: 'Deprecated: use config_variables instead. This field is kept - for backward compatibility.' - type: object - type: object BrickListItem: properties: author: diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 3248d129..d3090ff5 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -16,6 +16,7 @@ package bricks import ( + "cmp" "errors" "fmt" "log/slog" @@ -71,7 +72,7 @@ func (s *Service) List() (BrickListResult, error) { } func (s *Service) AppBrickInstancesList(a *app.ArduinoApp) (AppBrickInstancesResult, error) { - res := AppBrickInstancesResult{BrickInstances: make([]BrickInstanceListItem, len(a.Descriptor.Bricks))} + res := AppBrickInstancesResult{BrickInstances: make([]BrickInstance, len(a.Descriptor.Bricks))} for i, brickInstance := range a.Descriptor.Bricks { brick, found := s.bricksIndex.FindBrickByID(brickInstance.ID) if !found { @@ -80,16 +81,23 @@ func (s *Service) AppBrickInstancesList(a *app.ArduinoApp) (AppBrickInstancesRes variablesMap, configVariables := getInstanceBrickConfigVariableDetails(brick, brickInstance.Variables) - res.BrickInstances[i] = BrickInstanceListItem{ + res.BrickInstances[i] = BrickInstance{ ID: brick.ID, Name: brick.Name, Author: "Arduino", // TODO: for now we only support our bricks Category: brick.Category, Status: "installed", RequireModel: brick.RequireModel, - ModelID: brickInstance.Model, // TODO: in case is not set by the user, should we return the default model? - Variables: variablesMap, // TODO: do we want to show also the default value of not explicitly set variables? + ModelID: cmp.Or(brickInstance.Model, brick.ModelName), + Variables: variablesMap, ConfigVariables: configVariables, + CompatibleModels: f.Map(s.modelsIndex.GetModelsByBrick(brick.ID), func(m modelsindex.AIModel) AIModel { + return AIModel{ + ID: m.ID, + Name: m.Name, + Description: m.ModuleDescription, + } + }), } } @@ -109,11 +117,6 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br variables, configVariables := getInstanceBrickConfigVariableDetails(brick, a.Descriptor.Bricks[brickIndex].Variables) - modelID := a.Descriptor.Bricks[brickIndex].Model - if modelID == "" { - modelID = brick.ModelName - } - return BrickInstance{ ID: brickID, Name: brick.Name, @@ -123,7 +126,7 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br RequireModel: brick.RequireModel, Variables: variables, ConfigVariables: configVariables, - ModelID: modelID, + ModelID: cmp.Or(a.Descriptor.Bricks[brickIndex].Model, brick.ModelName), CompatibleModels: f.Map(s.modelsIndex.GetModelsByBrick(brick.ID), func(m modelsindex.AIModel) AIModel { return AIModel{ ID: m.ID, diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go index 332f5cb1..f52f36c6 100644 --- a/internal/orchestrator/bricks/bricks_test.go +++ b/internal/orchestrator/bricks/bricks_test.go @@ -733,7 +733,21 @@ func TestAppBrickInstancesList(t *testing.T) { svc := &Service{ bricksIndex: bIndex, - modelsIndex: &modelsindex.ModelsIndex{}, + modelsIndex: &modelsindex.ModelsIndex{ + Models: []modelsindex.AIModel{ + { + ID: "yolox-object-detection", + Name: "General purpose object detection - YoloX", + ModuleDescription: "a-model-description", + Bricks: []string{"arduino:object_detection"}, + }, + { + ID: "face-detection", + Name: "Lightweight-Face-Detection", + Bricks: []string{"arduino:object_detection"}, + }, + }, + }, } tests := []struct { @@ -809,6 +823,10 @@ func TestAppBrickInstancesList(t *testing.T) { require.Equal(t, "video", brick.Category) require.True(t, brick.RequireModel) require.Equal(t, "face-detection", brick.ModelID) + require.Equal(t, []AIModel{ + {ID: "yolox-object-detection", Name: "General purpose object detection - YoloX", Description: "a-model-description"}, + {ID: "face-detection", Name: "Lightweight-Face-Detection", Description: ""}, + }, brick.CompatibleModels) foundCustom := false for _, v := range brick.ConfigVariables { @@ -820,6 +838,30 @@ func TestAppBrickInstancesList(t *testing.T) { require.True(t, foundCustom, "Variable CUSTOM_MODEL_PATH should be present and overridden") }, }, + { + name: "Success - Brick using brick default model", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + { + ID: "arduino:object_detection", + }, + }, + }, + }, + validate: func(t *testing.T, res AppBrickInstancesResult) { + require.Len(t, res.BrickInstances, 1) + brick := res.BrickInstances[0] + + require.Equal(t, "arduino:object_detection", brick.ID) + require.True(t, brick.RequireModel) + require.Equal(t, "yolox-object-detection", brick.ModelID) + require.Equal(t, []AIModel{ + {ID: "yolox-object-detection", Name: "General purpose object detection - YoloX", Description: "a-model-description"}, + {ID: "face-detection", Name: "Lightweight-Face-Detection", Description: ""}, + }, brick.CompatibleModels) + }, + }, { name: "Success - Multiple Bricks", app: &app.ArduinoApp{ diff --git a/internal/orchestrator/bricks/types.go b/internal/orchestrator/bricks/types.go index e4b1b747..a0a28c0f 100644 --- a/internal/orchestrator/bricks/types.go +++ b/internal/orchestrator/bricks/types.go @@ -30,19 +30,9 @@ type BrickListItem struct { } type AppBrickInstancesResult struct { - BrickInstances []BrickInstanceListItem `json:"bricks"` -} -type BrickInstanceListItem struct { - ID string `json:"id"` - Name string `json:"name"` - Author string `json:"author"` - Category string `json:"category"` - Status string `json:"status"` - Variables map[string]string `json:"variables,omitempty" description:"Deprecated: use config_variables instead. This field is kept for backward compatibility."` - ConfigVariables []BrickConfigVariable `json:"config_variables,omitempty"` - RequireModel bool `json:"require_model"` - ModelID string `json:"model,omitempty"` + BrickInstances []BrickInstance `json:"bricks"` } + type BrickInstance struct { ID string `json:"id"` Name string `json:"name"`