feat: add miner detail view
This commit is contained in:
1 parent
b8b741ccee
commit
19fc29c72c
22 files changed
+1882
-10
No files matched your search
@@ -0,0 +1 @@
|
||||
"""Permission-aware Miner Detail read model."""
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Dependency construction for the Miner Detail composition service."""
|
||||
|
||||
from app.inventory.dependencies import get_inventory_service
|
||||
from app.locations.dependencies import get_location_map_service
|
||||
from app.miner_detail.service import MinerDetailService
|
||||
from app.repairs.dependencies import get_repair_center_service
|
||||
|
||||
|
||||
def get_miner_detail_service() -> MinerDetailService:
|
||||
return MinerDetailService(
|
||||
inventory_service=get_inventory_service(),
|
||||
location_service=get_location_map_service(),
|
||||
repair_service=get_repair_center_service(),
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Typed contracts for the Internal Alpha Miner Detail overview."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.inventory.models import InventoryMinerDetail
|
||||
from app.locations.models import MinerLocationDetail
|
||||
|
||||
|
||||
OperationalState = Literal[
|
||||
"online", "offline", "unknown", "unstable", "repair", "spare", "retired"
|
||||
]
|
||||
|
||||
|
||||
class MinerDetailAccess(BaseModel):
|
||||
can_view_locations: bool = False
|
||||
can_read_repairs: bool = False
|
||||
can_view_repairs: bool = False
|
||||
can_read_components: bool = False
|
||||
|
||||
|
||||
class MinerDetailLocationSection(BaseModel):
|
||||
available: bool
|
||||
restricted: bool
|
||||
location: MinerLocationDetail | None = None
|
||||
|
||||
|
||||
class MinerDetailRepairEpisode(BaseModel):
|
||||
repair_id: UUID | None = None
|
||||
source: Literal["modular", "legacy"]
|
||||
active: bool
|
||||
status: str
|
||||
reason: str | None = None
|
||||
opened_at: datetime | None = None
|
||||
closed_at: datetime | None = None
|
||||
latest_context: str | None = None
|
||||
latest_context_at: datetime | None = None
|
||||
parts_count: int | None = None
|
||||
parts_summary: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MinerDetailRepairSection(BaseModel):
|
||||
available: bool
|
||||
restricted: bool
|
||||
active_repair: MinerDetailRepairEpisode | None = None
|
||||
episodes: list[MinerDetailRepairEpisode] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MinerDetailCapabilities(BaseModel):
|
||||
can_view_map: bool = False
|
||||
can_open_repair_center: bool = False
|
||||
can_read_repair_history: bool = False
|
||||
can_read_components: bool = False
|
||||
|
||||
|
||||
class MinerDetailOverviewData(BaseModel):
|
||||
miner: InventoryMinerDetail
|
||||
operational_state: OperationalState
|
||||
location: MinerDetailLocationSection
|
||||
repair: MinerDetailRepairSection
|
||||
capabilities: MinerDetailCapabilities
|
||||
|
||||
|
||||
class MinerDetailOverviewMeta(BaseModel):
|
||||
generated_at: datetime
|
||||
database_query_count: int = Field(ge=1, le=3)
|
||||
|
||||
|
||||
class MinerDetailOverviewResponse(BaseModel):
|
||||
data: MinerDetailOverviewData
|
||||
meta: MinerDetailOverviewMeta
|
||||
error: None = None
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""HTTP boundary for the Internal Alpha Miner Detail overview."""
|
||||
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
from app.identity import require_permission, user_has_permission
|
||||
from app.inventory.exceptions import MinerNotFound
|
||||
from app.miner_detail.dependencies import get_miner_detail_service
|
||||
from app.miner_detail.models import (
|
||||
MinerDetailAccess,
|
||||
MinerDetailOverviewResponse,
|
||||
)
|
||||
from app.miner_detail.service import MinerDetailService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/miners", tags=["miners"])
|
||||
MinerDetailUser = Annotated[dict, Depends(require_permission("miners.view"))]
|
||||
MinerDetailServiceDependency = Annotated[
|
||||
MinerDetailService,
|
||||
Depends(get_miner_detail_service),
|
||||
]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{miner_id}/overview",
|
||||
response_model=MinerDetailOverviewResponse,
|
||||
)
|
||||
def get_miner_detail_overview(
|
||||
miner_id: UUID,
|
||||
current_user: MinerDetailUser,
|
||||
service: MinerDetailServiceDependency,
|
||||
response: Response,
|
||||
) -> MinerDetailOverviewResponse:
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
try:
|
||||
return service.get_overview(
|
||||
miner_id,
|
||||
MinerDetailAccess(
|
||||
can_view_locations=user_has_permission(
|
||||
current_user, "locations.view"
|
||||
),
|
||||
can_read_repairs=user_has_permission(
|
||||
current_user, "repairs.read"
|
||||
),
|
||||
can_view_repairs=user_has_permission(
|
||||
current_user, "repairs.view"
|
||||
),
|
||||
can_read_components=user_has_permission(
|
||||
current_user, "components.read"
|
||||
),
|
||||
),
|
||||
)
|
||||
except MinerNotFound as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Miner not found",
|
||||
) from exc
|
||||
@@ -0,0 +1,152 @@
|
||||
"""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,
|
||||
),
|
||||
)
|
||||
Reference in new issue
Block a user