feat: add operational Dashboard

This commit is contained in:
lussierj committed 2026-08-17 17:11:40 +00:00
1 parent 4a1959ecd6
commit b8b741ccee
23 files changed
+1278 -59

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Permission-aware operational Dashboard read model."""
+14
View File
@@ -0,0 +1,14 @@
"""Dependency construction for the Dashboard composition service."""
from app.dashboard.service import DashboardService
from app.inventory.dependencies import get_inventory_service
from app.locations.dependencies import get_location_map_service
from app.repairs.dependencies import get_repair_center_service
def get_dashboard_service() -> DashboardService:
return DashboardService(
inventory_service=get_inventory_service(),
repair_center_service=get_repair_center_service(),
location_service=get_location_map_service(),
)
+32
View File
@@ -0,0 +1,32 @@
"""Typed contracts for the operational Dashboard overview."""
from datetime import datetime
from pydantic import BaseModel, Field
from app.inventory.models import InventoryFleetSummary
from app.locations.models import LocationSummary
from app.repairs.models import ActiveRepairSummary
class DashboardAccess(BaseModel):
can_view_miners: bool = False
can_view_repairs: bool = False
can_view_locations: bool = False
class DashboardOverviewData(BaseModel):
fleet: InventoryFleetSummary | None = None
repairs: ActiveRepairSummary | None = None
locations: LocationSummary | None = None
generated_at: datetime
class DashboardOverviewMeta(BaseModel):
available_sections: list[str] = Field(default_factory=list)
class DashboardOverviewResponse(BaseModel):
data: DashboardOverviewData
meta: DashboardOverviewMeta = Field(default_factory=DashboardOverviewMeta)
error: None = None
+40
View File
@@ -0,0 +1,40 @@
"""HTTP boundary for the operational Dashboard overview."""
from typing import Annotated
from fastapi import APIRouter, Depends, Response
from app.dashboard.dependencies import get_dashboard_service
from app.dashboard.models import (
DashboardAccess,
DashboardOverviewResponse,
)
from app.dashboard.service import DashboardService
from app.identity import require_permission, user_has_permission
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
DashboardUser = Annotated[dict, Depends(require_permission("dashboard.view"))]
DashboardServiceDependency = Annotated[
DashboardService,
Depends(get_dashboard_service),
]
@router.get("/overview", response_model=DashboardOverviewResponse)
def get_dashboard_overview(
current_user: DashboardUser,
service: DashboardServiceDependency,
response: Response,
) -> DashboardOverviewResponse:
response.headers["Cache-Control"] = "no-store"
return service.get_overview(
DashboardAccess(
can_view_miners=user_has_permission(current_user, "miners.view"),
can_view_repairs=user_has_permission(current_user, "repairs.view"),
can_view_locations=user_has_permission(
current_user,
"locations.view",
),
)
)
+64
View File
@@ -0,0 +1,64 @@
"""Composition service for permission-aware Dashboard summaries."""
from datetime import datetime, timezone
from app.dashboard.models import (
DashboardAccess,
DashboardOverviewData,
DashboardOverviewMeta,
DashboardOverviewResponse,
)
from app.inventory.service import InventoryService
from app.locations.service import LocationMapService
from app.repairs.center_service import RepairCenterService
class DashboardService:
"""Coordinate domain-owned summaries without introducing new rules."""
def __init__(
self,
inventory_service: InventoryService,
repair_center_service: RepairCenterService,
location_service: LocationMapService,
):
self.inventory_service = inventory_service
self.repair_center_service = repair_center_service
self.location_service = location_service
def get_overview(self, access: DashboardAccess) -> DashboardOverviewResponse:
fleet = (
self.inventory_service.summarize_fleet()
if access.can_view_miners
else None
)
repairs = (
self.repair_center_service.summarize_active_repairs()
if access.can_view_repairs
else None
)
locations = (
self.location_service.summarize_locations(
include_miner_counts=access.can_view_miners
)
if access.can_view_locations
else None
)
available_sections = [
name
for name, value in (
("fleet", fleet),
("repairs", repairs),
("locations", locations),
)
if value is not None
]
return DashboardOverviewResponse(
data=DashboardOverviewData(
fleet=fleet,
repairs=repairs,
locations=locations,
generated_at=datetime.now(timezone.utc),
),
meta=DashboardOverviewMeta(available_sections=available_sections),
)