From 947b2858e290a22e32c856d754afd57a3ecf618b Mon Sep 17 00:00:00 2001 From: Marco Mancino Date: Wed, 1 Jul 2026 00:46:38 +0200 Subject: [PATCH 1/3] feat(miner): configure and order controller features at creation time Allow setting feature priority/enabled state in the Add Miner form before the miner is saved, instead of only after re-opening it in edit mode. - Add GET /miner-controllers/{id}/supported-features endpoint and MinerActionService.get_controller_supported_features to expose the feature types a controller provides without a persisted miner. - In the miner form, generate placeholder features (enabled, priority 50) when a controller is selected so they appear immediately and can be reordered. - On save, apply the chosen priority/enabled values after controllers are linked; works for new miners and for controllers newly added to existing ones. Closes #48 --- CHANGELOG.md | 22 ++++++++ .../adapters/domain/miner/fast_api/router.py | 16 ++++++ core/edge_mining/application/interfaces.py | 4 ++ .../services/miner_action_service.py | 28 ++++++++++ .../src/components/miners/MinerFormModal.vue | 54 ++++++++++++++----- frontend/src/core/services/minerService.ts | 4 ++ .../src/views/settings/MinersSettingsView.vue | 19 +++++-- 7 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..e9947df0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Configure and order controller features directly in the miner creation form, + before the miner is saved (#48): + - New REST endpoint `GET /miner-controllers/{controller_id}/supported-features` + returning the feature types a controller supports, without requiring a + persisted miner (`MinerActionService.get_controller_supported_features`). + - The "Add Miner" form now shows a controller's features as soon as it is + selected (with the backend defaults: enabled, priority 50), so priority and + enabled/disabled state can be set during creation. + - On save, the chosen priority/enabled values are applied after the controllers + are linked. This also works for controllers newly added to an existing miner, + without requiring a second save. diff --git a/core/edge_mining/adapters/domain/miner/fast_api/router.py b/core/edge_mining/adapters/domain/miner/fast_api/router.py index 341d94c9..a35f4254 100644 --- a/core/edge_mining/adapters/domain/miner/fast_api/router.py +++ b/core/edge_mining/adapters/domain/miner/fast_api/router.py @@ -659,6 +659,22 @@ async def get_miner_details_from_controller( raise HTTPException(status_code=500, detail=str(e)) from e +@router.get("/miner-controllers/{controller_id}/supported-features", response_model=List[MinerFeatureType]) +async def get_controller_supported_features( + controller_id: EntityId, + action_service: Annotated[MinerActionServiceInterface, Depends(get_miner_action_service)], +) -> List[MinerFeatureType]: + """Get the feature types a controller supports, without requiring a persisted miner.""" + try: + return await action_service.get_controller_supported_features(controller_id) + except MinerControllerNotFoundError as e: + raise HTTPException(status_code=404, detail="Miner controller not found") from e + except MinerControllerConfigurationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + @router.put("/miner-controllers/{controller_id}", response_model=MinerControllerSchema) async def update_miner_controller( controller_id: EntityId, diff --git a/core/edge_mining/application/interfaces.py b/core/edge_mining/application/interfaces.py index c41c9cb1..f8aff669 100644 --- a/core/edge_mining/application/interfaces.py +++ b/core/edge_mining/application/interfaces.py @@ -230,6 +230,10 @@ async def sync_all_miners(self, include_inactive: bool = False) -> None: async def get_miner_details_from_controller(self, controller_id: EntityId) -> Optional[MinerStateSnapshot]: """Get details of a miner from its controller as a state snapshot.""" + @abstractmethod + async def get_controller_supported_features(self, controller_id: EntityId) -> List[MinerFeatureType]: + """Get the feature types supported by a controller, without requiring a persisted miner.""" + class ConfigurationServiceInterface(ABC): """Base interface for configuration services in the Edge Mining application.""" diff --git a/core/edge_mining/application/services/miner_action_service.py b/core/edge_mining/application/services/miner_action_service.py index ced94349..c75dd619 100644 --- a/core/edge_mining/application/services/miner_action_service.py +++ b/core/edge_mining/application/services/miner_action_service.py @@ -482,3 +482,31 @@ async def get_miner_details_from_controller(self, controller_id: EntityId) -> Mi self.logger.debug(f"Retrieved miner details for controller {controller_id}") return snapshot + + async def get_controller_supported_features(self, controller_id: EntityId) -> List[MinerFeatureType]: + """Get the feature types supported by a controller, without requiring a persisted miner. + + Resolves the controller adapter (via a temporary miner, like + ``get_miner_details_from_controller``) and introspects its supported + feature types. This allows the UI to preview/configure features before + the miner is saved. + """ + if self.logger: + self.logger.info(f"Getting supported features for controller {controller_id}") + + # Build a temporary miner so the adapter service can resolve the controller + temp_features = [MinerFeature(feature_type=ft, controller_id=controller_id) for ft in MinerFeatureType] + temp_miner = Miner( + name="Unknown", + model="Unknown", + hash_rate_max=None, + power_consumption_max=None, + active=True, + features=temp_features, + ) + + adapter = await self.adapter_service.get_miner_controller_adapter(temp_miner, controller_id) + if not adapter: + raise MinerControllerConfigurationError(f"Could not initialize adapter for controller {controller_id}.") + + return list(adapter.__class__.get_supported_features()) diff --git a/frontend/src/components/miners/MinerFormModal.vue b/frontend/src/components/miners/MinerFormModal.vue index fe18c1b0..d33bdb29 100644 --- a/frontend/src/components/miners/MinerFormModal.vue +++ b/frontend/src/components/miners/MinerFormModal.vue @@ -120,16 +120,50 @@ const availableControllers = computed(() => { // Controller ID being added from the dropdown const controllerToAdd = ref(undefined); - -function addController() { - if (controllerToAdd.value && !selectedControllerIds.value.includes(controllerToAdd.value)) { - selectedControllerIds.value.push(controllerToAdd.value); - controllerToAdd.value = undefined; - } +const addingController = ref(false); + +async function addController() { + const controllerId = controllerToAdd.value; + if (!controllerId || selectedControllerIds.value.includes(controllerId)) return; + selectedControllerIds.value.push(controllerId); + controllerToAdd.value = undefined; + await ensureFeaturesForController(controllerId); } function removeController(controllerId: string) { selectedControllerIds.value = selectedControllerIds.value.filter((id) => id !== controllerId); + // Drop the controller's features from both working and baseline copies. The + // backend recreates them at default (enabled, priority 50) if it is re-linked. + localFeatures.value = localFeatures.value.filter((f) => f.controller_id !== controllerId); + originalFeatures.value = originalFeatures.value.filter((f) => f.controller_id !== controllerId); +} + +// Generate placeholder features for a freshly-selected controller so they can be +// configured/ordered before the miner is saved. The backend auto-creates the +// same set (enabled, priority 50) when the controller is linked on save. +async function ensureFeaturesForController(controllerId: string) { + // Already have features for this controller (e.g. loaded from an existing miner) + if (localFeatures.value.some((f) => f.controller_id === controllerId)) return; + addingController.value = true; + fetchError.value = null; + try { + const featureTypes = await minerService.getControllerSupportedFeatures(controllerId); + const synthetic: MinerFeature[] = featureTypes.map((ft) => ({ + feature_type: ft as MinerFeatureType, + controller_id: controllerId, + priority: 50, + enabled: true, + })); + localFeatures.value.push(...synthetic); + // Record the defaults as baseline so the "modified" badge and the save-time + // diff only reflect changes the user actually makes. + originalFeatures.value.push(...synthetic.map((f) => ({ ...f }))); + } catch (error: any) { + fetchError.value = + error?.response?.data?.detail || error?.message || "Failed to load controller features"; + } finally { + addingController.value = false; + } } function getControllerName(controllerId: string): string { @@ -522,11 +556,11 @@ function handleSave() {