Files
minerops/backend/app/miner_detail/service.py
T
2026-08-17 18:43:20 +00:00

153 lines
5.5 KiB
Python

"""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,
),
)