"""Mine Map read-model composition independent of HTTP.""" from datetime import datetime, timezone from typing import Any from uuid import UUID from app.locations.models import ( LocationSummary, MapActiveRepair, MapPosition, MapRoom, MapSite, MapVirtualMiner, MineMapData, MineMapMeta, MineMapResponse, MinerLocationDetail, ) from app.locations.repository import LocationMapRepository MINER_FIELDS = { "miner_id", "asset_id", "current_mac", "expected_mac", "current_ip", "miner_status", "status", "hostname", "miner_sn", "power_sn", "model", "miner_type", "firmware_version", "lifecycle_status", "last_seen_at", "asset_state", "observed_at", "hashrate_ths", "power_w", "chip_temp_max_c", "chip_temp_avg_c", "hash_stable", "pool_rejected_percent", } class LocationMapService: def __init__(self, repository: LocationMapRepository): self.repository = repository @staticmethod def _repair(row: dict[str, Any]) -> MapActiveRepair | None: repair_id = row.pop("active_repair_id", None) status = row.pop("active_repair_status", None) reason = row.pop("active_repair_reason", None) if repair_id is None: return None return MapActiveRepair(id=repair_id, status=status, reason=reason) @classmethod def _redact(cls, row: dict[str, Any]) -> dict[str, Any]: redacted = dict(row) for field in MINER_FIELDS: redacted[field] = None redacted["active_repair_id"] = None redacted["active_repair_status"] = None redacted["active_repair_reason"] = None return redacted def get_map( self, *, site_id: UUID | None = None, room_id: UUID | None = None, include_miner_details: bool, ) -> MineMapResponse: rows = self.repository.read_map(site_id=site_id, room_id=room_id) positions = [] for source in rows["positions"]: row = dict(source) if include_miner_details else self._redact(source) row["active_repair"] = self._repair(row) positions.append(MapPosition.model_validate(row)) virtual_miners = [] if include_miner_details: for source in rows["virtual_miners"]: row = dict(source) row["active_repair"] = self._repair(row) virtual_miners.append(MapVirtualMiner.model_validate(row)) return MineMapResponse( data=MineMapData( sites=[MapSite.model_validate(row) for row in rows["sites"]], rooms=[MapRoom.model_validate(row) for row in rows["rooms"]], positions=positions, virtual_miners=virtual_miners, ), meta=MineMapMeta( generated_at=datetime.now(timezone.utc), site_id=site_id, room_id=room_id, position_count=len(positions), virtual_miner_count=len(virtual_miners), miner_details_included=include_miner_details, ), ) def get_miner_location(self, miner_id: UUID) -> MinerLocationDetail | None: row = self.repository.get_miner_location(miner_id) if row is None: return None return MinerLocationDetail.model_validate(row) def summarize_locations(self, *, include_miner_counts: bool) -> LocationSummary: return LocationSummary.model_validate( self.repository.summarize_locations( include_miner_counts=include_miner_counts ) )