feat: Enhance project and scenario creation with monitoring metrics
Some checks failed
CI / lint (push) Failing after 1m14s
CI / test (push) Has been skipped
CI / build (push) Has been skipped

- Added monitoring metrics for project creation success and error handling in `ProjectRepository`.
- Implemented similar monitoring for scenario creation in `ScenarioRepository`.
- Refactored `run_monte_carlo` function in `simulation.py` to include timing and success/error metrics.
- Introduced new CSS styles for headers, alerts, and navigation buttons in `main.css` and `projects.css`.
- Created a new JavaScript file for navigation logic to handle chevron buttons.
- Updated HTML templates to include new navigation buttons and improved styling for buttons.
- Added tests for reporting service and routes to ensure proper functionality and access control.
- Removed unused imports and optimized existing test files for better clarity and performance.
This commit is contained in:
2025-11-12 10:36:24 +01:00
parent f68321cd04
commit ce9c174b53
61 changed files with 2124 additions and 308 deletions

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import date
from datetime import date, datetime
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
@@ -12,7 +12,6 @@ from dependencies import (
get_unit_of_work,
require_any_role,
require_project_resource,
require_roles,
require_scenario_resource,
)
from models import Project, Scenario, User
@@ -30,6 +29,93 @@ from services.unit_of_work import UnitOfWork
router = APIRouter(prefix="/reports", tags=["Reports"])
templates = Jinja2Templates(directory="templates")
# Add custom Jinja2 filters
def format_datetime(value):
"""Format a datetime object for display in templates."""
if not isinstance(value, datetime):
return ""
if value.tzinfo is None:
# Assume UTC if no timezone
from datetime import timezone
value = value.replace(tzinfo=timezone.utc)
# Format as readable date/time
return value.strftime("%Y-%m-%d %H:%M UTC")
def currency_display(value, currency_code):
"""Format a numeric value with currency symbol/code."""
if value is None:
return ""
# Format the number
if isinstance(value, (int, float)):
formatted_value = f"{value:,.2f}"
else:
formatted_value = str(value)
# Add currency code
if currency_code:
return f"{currency_code} {formatted_value}"
return formatted_value
def format_metric(value, metric_name, currency_code=None):
"""Format metric values appropriately based on metric type."""
if value is None:
return ""
# For currency-related metrics, use currency formatting
currency_metrics = {'npv', 'inflows', 'outflows',
'net', 'total_inflows', 'total_outflows', 'total_net'}
if metric_name in currency_metrics and currency_code:
return currency_display(value, currency_code)
# For percentage metrics
percentage_metrics = {'irr', 'payback_period'}
if metric_name in percentage_metrics:
if isinstance(value, (int, float)):
return f"{value:.2f}%"
return f"{value}%"
# Default numeric formatting
if isinstance(value, (int, float)):
return f"{value:,.2f}"
return str(value)
def percentage_display(value):
"""Format a value as a percentage."""
if value is None:
return ""
if isinstance(value, (int, float)):
return f"{value:.2f}%"
return f"{value}%"
def period_display(value):
"""Format a period value (like payback period)."""
if value is None:
return ""
if isinstance(value, (int, float)):
if value == int(value):
return f"{int(value)} years"
return f"{value:.1f} years"
return str(value)
templates.env.filters['format_datetime'] = format_datetime
templates.env.filters['currency_display'] = currency_display
templates.env.filters['format_metric'] = format_metric
templates.env.filters['percentage_display'] = percentage_display
templates.env.filters['period_display'] = period_display
READ_ROLES = ("viewer", "analyst", "project_manager", "admin")
MANAGE_ROLES = ("project_manager", "admin")
@@ -296,35 +382,9 @@ def project_summary_page(
)
service = ReportingService(uow)
report = service.project_summary(
project,
filters=scenario_filter,
include=include_options,
iterations=iterations or DEFAULT_ITERATIONS,
percentiles=percentile_values,
context = service.build_project_summary_context(
project, scenario_filter, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
)
context = {
"request": request,
"project": report["project"],
"scenario_count": report["scenario_count"],
"aggregates": report["aggregates"],
"scenarios": report["scenarios"],
"filters": report["filters"],
"include_options": include_options,
"iterations": iterations or DEFAULT_ITERATIONS,
"percentiles": percentile_values,
"title": f"Project Summary · {project.name}",
"subtitle": "Aggregated financial and simulation insights across scenarios.",
"actions": [
{
"href": request.url_for(
"reports.project_summary",
project_id=project.id,
),
"label": "Download JSON",
}
],
}
return templates.TemplateResponse(
request,
"reports/project_summary.html",
@@ -399,40 +459,9 @@ def project_scenario_comparison_page(
)
service = ReportingService(uow)
report = service.scenario_comparison(
project,
scenarios,
include=include_options,
iterations=iterations or DEFAULT_ITERATIONS,
percentiles=percentile_values,
context = service.build_scenario_comparison_context(
project, scenarios, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
)
comparison_json_url = request.url_for(
"reports.project_scenario_comparison",
project_id=project.id,
)
comparison_query = urlencode(
[("scenario_ids", str(identifier)) for identifier in unique_ids]
)
if comparison_query:
comparison_json_url = f"{comparison_json_url}?{comparison_query}"
context = {
"request": request,
"project": report["project"],
"scenarios": report["scenarios"],
"comparison": report["comparison"],
"include_options": include_options,
"iterations": iterations or DEFAULT_ITERATIONS,
"percentiles": percentile_values,
"title": f"Scenario Comparison · {project.name}",
"subtitle": "Evaluate deterministic metrics and Monte Carlo trends side by side.",
"actions": [
{
"href": comparison_json_url,
"label": "Download JSON",
}
],
}
return templates.TemplateResponse(
request,
"reports/scenario_comparison.html",
@@ -478,33 +507,9 @@ def scenario_distribution_page(
) from exc
service = ReportingService(uow)
report = service.scenario_distribution(
scenario,
include=include_options,
iterations=iterations or DEFAULT_ITERATIONS,
percentiles=percentile_values,
context = service.build_scenario_distribution_context(
scenario, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
)
context = {
"request": request,
"scenario": report["scenario"],
"summary": report["summary"],
"metrics": report["metrics"],
"monte_carlo": report["monte_carlo"],
"include_options": include_options,
"iterations": iterations or DEFAULT_ITERATIONS,
"percentiles": percentile_values,
"title": f"Scenario Distribution · {scenario.name}",
"subtitle": "Deterministic and simulated distributions for a single scenario.",
"actions": [
{
"href": request.url_for(
"reports.scenario_distribution",
scenario_id=scenario.id,
),
"label": "Download JSON",
}
],
}
return templates.TemplateResponse(
request,
"reports/scenario_distribution.html",