feat: enhance dashboard with new metrics, project and scenario utilities, and comprehensive tests
This commit is contained in:
@@ -1,83 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from dependencies import get_unit_of_work
|
||||
from models import MiningOperationType
|
||||
from models import ScenarioStatus
|
||||
from services.unit_of_work import UnitOfWork
|
||||
|
||||
router = APIRouter(tags=["Dashboard"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
def _load_metrics(_: UnitOfWork) -> dict[str, object]:
|
||||
today = datetime.utcnow()
|
||||
def _format_timestamp(moment: datetime | None) -> str | None:
|
||||
if moment is None:
|
||||
return None
|
||||
return moment.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _format_timestamp_with_time(moment: datetime | None) -> str | None:
|
||||
if moment is None:
|
||||
return None
|
||||
return moment.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def _load_metrics(uow: UnitOfWork) -> dict[str, object]:
|
||||
total_projects = uow.projects.count()
|
||||
active_scenarios = uow.scenarios.count_by_status(ScenarioStatus.ACTIVE)
|
||||
pending_simulations = uow.scenarios.count_by_status(ScenarioStatus.DRAFT)
|
||||
last_import_at = uow.financial_inputs.latest_created_at()
|
||||
return {
|
||||
"total_projects": 12,
|
||||
"active_scenarios": 7,
|
||||
"pending_simulations": 3,
|
||||
"last_import": today.strftime("%Y-%m-%d"),
|
||||
"total_projects": total_projects,
|
||||
"active_scenarios": active_scenarios,
|
||||
"pending_simulations": pending_simulations,
|
||||
"last_import": _format_timestamp(last_import_at),
|
||||
}
|
||||
|
||||
|
||||
def _load_recent_projects(_: UnitOfWork) -> list[SimpleNamespace]:
|
||||
now = datetime.utcnow()
|
||||
return [
|
||||
SimpleNamespace(
|
||||
id=1,
|
||||
name="Copper Ridge Expansion",
|
||||
operation_type=MiningOperationType.OPEN_PIT,
|
||||
updated_at=now - timedelta(days=2),
|
||||
),
|
||||
SimpleNamespace(
|
||||
id=2,
|
||||
name="Lithium Basin North",
|
||||
operation_type=MiningOperationType.UNDERGROUND,
|
||||
updated_at=now - timedelta(days=5),
|
||||
),
|
||||
SimpleNamespace(
|
||||
id=3,
|
||||
name="Nickel Underground Phase II",
|
||||
operation_type=MiningOperationType.IN_SITU_LEACH,
|
||||
updated_at=now - timedelta(days=9),
|
||||
),
|
||||
]
|
||||
def _load_recent_projects(uow: UnitOfWork) -> list:
|
||||
return list(uow.projects.recent(limit=5))
|
||||
|
||||
|
||||
def _load_simulation_updates(_: UnitOfWork) -> list[SimpleNamespace]:
|
||||
now = datetime.utcnow()
|
||||
return [
|
||||
SimpleNamespace(
|
||||
title="Monte Carlo Batch #21 completed",
|
||||
description="1,000 runs processed for Lithium Basin North.",
|
||||
timestamp=now - timedelta(hours=4),
|
||||
),
|
||||
SimpleNamespace(
|
||||
title="Scenario validation queued",
|
||||
description="Copper Ridge Expansion pending validation on new cost inputs.",
|
||||
timestamp=now - timedelta(days=1, hours=3),
|
||||
),
|
||||
]
|
||||
def _load_simulation_updates(uow: UnitOfWork) -> list[dict[str, object]]:
|
||||
updates: list[dict[str, object]] = []
|
||||
scenarios = uow.scenarios.recent(limit=5, with_project=True)
|
||||
for scenario in scenarios:
|
||||
project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}"
|
||||
timestamp_label = _format_timestamp_with_time(scenario.updated_at)
|
||||
updates.append(
|
||||
{
|
||||
"title": f"{scenario.name} · {scenario.status.value.title()}",
|
||||
"description": f"Latest update recorded for {project_name}.",
|
||||
"timestamp": scenario.updated_at,
|
||||
"timestamp_label": timestamp_label,
|
||||
}
|
||||
)
|
||||
return updates
|
||||
|
||||
|
||||
def _load_scenario_alerts(_: UnitOfWork) -> list[SimpleNamespace]:
|
||||
return [
|
||||
SimpleNamespace(
|
||||
title="Variance exceeds threshold",
|
||||
message="Nickel Underground Phase II deviates 18% from baseline forecast.",
|
||||
link="/projects/3/view",
|
||||
),
|
||||
SimpleNamespace(
|
||||
title="Simulation backlog",
|
||||
message="Lithium Basin North has 2 pending simulation batches.",
|
||||
link="/projects/2/view",
|
||||
),
|
||||
]
|
||||
def _load_scenario_alerts(
|
||||
request: Request, uow: UnitOfWork
|
||||
) -> list[dict[str, object]]:
|
||||
alerts: list[dict[str, object]] = []
|
||||
|
||||
drafts = uow.scenarios.list_by_status(
|
||||
ScenarioStatus.DRAFT, limit=3, with_project=True
|
||||
)
|
||||
for scenario in drafts:
|
||||
project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}"
|
||||
alerts.append(
|
||||
{
|
||||
"title": f"Draft scenario: {scenario.name}",
|
||||
"message": f"{project_name} has a scenario awaiting validation.",
|
||||
"link": request.url_for(
|
||||
"projects.view_project", project_id=scenario.project_id
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if not alerts:
|
||||
archived = uow.scenarios.list_by_status(
|
||||
ScenarioStatus.ARCHIVED, limit=3, with_project=True
|
||||
)
|
||||
for scenario in archived:
|
||||
project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}"
|
||||
alerts.append(
|
||||
{
|
||||
"title": f"Archived scenario: {scenario.name}",
|
||||
"message": f"Review archived scenario insights for {project_name}.",
|
||||
"link": request.url_for(
|
||||
"scenarios.view_scenario", scenario_id=scenario.id
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return alerts
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False, name="dashboard.home")
|
||||
@@ -90,6 +109,6 @@ def dashboard_home(
|
||||
"metrics": _load_metrics(uow),
|
||||
"recent_projects": _load_recent_projects(uow),
|
||||
"simulation_updates": _load_simulation_updates(uow),
|
||||
"scenario_alerts": _load_scenario_alerts(uow),
|
||||
"scenario_alerts": _load_scenario_alerts(request, uow),
|
||||
}
|
||||
return templates.TemplateResponse("dashboard.html", context)
|
||||
|
||||
Reference in New Issue
Block a user