"""Composition service for the Internal Alpha Miner Detail overview.""" from datetime import datetime, timezone from math import isfinite from uuid import UUID from app.inventory.models import InventoryMinerDetail from app.inventory.service import InventoryService from app.locations.service import LocationMapService from app.miner_detail.models import ( MinerDetailAccess, MinerDetailCapabilities, MinerDetailLocationSection, MinerDetailOverviewData, MinerDetailOverviewMeta, MinerDetailOverviewResponse, MinerDetailRepairEpisode, MinerDetailRepairSection, OperationalState, ) from app.repairs.center_service import RepairCenterService from app.repairs.models import RepairCenterAccess, RepairCenterItem class MinerDetailService: """Coordinate domain-owned current projections without transport logic.""" def __init__( self, inventory_service: InventoryService, location_service: LocationMapService, repair_service: RepairCenterService, ): self.inventory_service = inventory_service self.location_service = location_service self.repair_service = repair_service @staticmethod def _episode(item: RepairCenterItem) -> MinerDetailRepairEpisode: return MinerDetailRepairEpisode( repair_id=item.repair_id, source=item.source, active=item.active, status=item.repair_status, reason=item.reason, opened_at=item.opened_at, closed_at=item.closed_at, latest_context=item.latest_context, latest_context_at=item.latest_context_at, parts_count=item.parts_count, parts_summary=item.parts_summary, ) @staticmethod def _operational_state( miner: InventoryMinerDetail, active_repair: MinerDetailRepairEpisode | None, ) -> OperationalState: if active_repair is not None or miner.lifecycle_status == "in_repair": return "repair" if miner.lifecycle_status == "retired": return "retired" if miner.lifecycle_status == "spare": return "spare" runtime = (miner.runtime_status or "").strip().lower() if runtime == "offline": return "offline" if miner.hash_stable is False: return "unstable" if runtime == "online": return "online" return "unknown" def get_overview( self, miner_id: UUID, access: MinerDetailAccess, ) -> MinerDetailOverviewResponse: miner = self.inventory_service.get_current_miner(miner_id) # Location is owned by the permission-aware section below. Keep the # shared projection from leaking location identifiers. Non-finite # telemetry is not a factual display value and remains unavailable. redacted = { field: None for field in ( "site_id", "site_name", "room_id", "room_name", "location_position_id", "rack_label", "shelf_label", "position_label", "position_index", ) } for field in ("hashrate_ths", "power_w", "chip_temp_max_c"): value = getattr(miner, field) if value is not None and not isfinite(value): redacted[field] = None miner = miner.model_copy( update=redacted ) location = ( self.location_service.get_miner_location(miner_id) if access.can_view_locations else None ) repair_items = ( self.repair_service.list_miner_repairs( miner_id, RepairCenterAccess( can_read_repairs=True, can_view_miners=True, ), ) if access.can_read_repairs else [] ) episodes = [self._episode(item) for item in repair_items] active_repair = next( ( episode for episode in episodes if episode.active and episode.source == "modular" ), None, ) query_count = 1 + int(access.can_view_locations) + int(access.can_read_repairs) return MinerDetailOverviewResponse( data=MinerDetailOverviewData( miner=miner, operational_state=self._operational_state(miner, active_repair), location=MinerDetailLocationSection( available=access.can_view_locations, restricted=not access.can_view_locations, location=location, ), repair=MinerDetailRepairSection( available=access.can_read_repairs, restricted=not access.can_read_repairs, active_repair=active_repair, episodes=episodes, ), capabilities=MinerDetailCapabilities( can_view_map=bool( access.can_view_locations and location is not None and location.site_id and location.room_id ), can_open_repair_center=access.can_view_repairs, can_read_repair_history=access.can_read_repairs, can_read_components=access.can_read_components, ), ), meta=MinerDetailOverviewMeta( generated_at=datetime.now(timezone.utc), database_query_count=query_count, ), )