diff --git a/changelog.md b/changelog.md index 87a83b3..54bce5c 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,11 @@ ## 2025-11-12 +- Fixed critical 500 error in reporting dashboard by correcting route reference in reporting.html template - changed 'reports.project_list_page' to 'projects.project_list_page' to resolve NoMatchFound error when accessing /ui/reporting. +- Completed navigation validation by inventorying all sidebar navigation links, identifying missing routes for simulations, reporting, settings, themes, and currencies, created new UI routes in routes/ui.py with proper authentication guards, built corresponding templates (simulations.html, reporting.html, settings.html, theme_settings.html, currencies.html), registered the UI router in main.py, updated sidebar navigation to use route names instead of hardcoded URLs, and enhanced navigation.js to use dynamic URL resolution for proper route handling. +- Fixed critical template rendering error in sidebar_nav.html where URL objects from request.url_for() were being used with string methods, causing TypeError. Added |string filters to convert URL objects to strings for proper template rendering. +- Integrated Plotly charting for interactive visualizations in reporting templates, added chart generation methods to ReportingService (\_generate_npv_comparison_chart, \_generate_distribution_histogram), updated project summary and scenario distribution contexts to include chart JSON data, enhanced templates with chart containers and JavaScript rendering, added chart-container CSS styling, and validated all reporting tests pass. + - Completed local run verification: started application with `uvicorn main:app --reload` without errors, verified authenticated routes (/login, /, /projects/ui, /projects) load correctly with seeded data, and summarized findings for deployment pipeline readiness. - Fixed docker-compose.override.yml command array to remove duplicate "uvicorn" entry, enabling successful container startup with uvicorn reload in development mode. - Completed deployment pipeline verification: built Docker image without errors, validated docker-compose configuration, deployed locally with docker-compose (app and postgres containers started successfully), and confirmed application startup logs showing database bootstrap and seeded data initialization. diff --git a/main.py b/main.py index 62973e7..6d817c3 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,7 @@ from routes.exports import router as exports_router from routes.projects import router as projects_router from routes.reports import router as reports_router from routes.scenarios import router as scenarios_router +from routes.ui import router as ui_router from monitoring import router as monitoring_router from services.bootstrap import bootstrap_admin, bootstrap_pricing_settings from scripts.init_db import init_db as init_db_script @@ -98,6 +99,7 @@ app.include_router(exports_router) app.include_router(projects_router) app.include_router(scenarios_router) app.include_router(reports_router) +app.include_router(ui_router) app.include_router(monitoring_router) app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/routes/ui.py b/routes/ui.py new file mode 100644 index 0000000..d152d42 --- /dev/null +++ b/routes/ui.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from dependencies import require_any_role, require_roles +from models import User + +router = APIRouter(tags=["UI"]) +templates = Jinja2Templates(directory="templates") + +READ_ROLES = ("viewer", "analyst", "project_manager", "admin") +MANAGE_ROLES = ("project_manager", "admin") + + +@router.get( + "/ui/simulations", + response_class=HTMLResponse, + include_in_schema=False, + name="ui.simulations", +) +def simulations_dashboard( + request: Request, + _: User = Depends(require_any_role(*READ_ROLES)), +) -> HTMLResponse: + return templates.TemplateResponse( + request, + "simulations.html", + { + "title": "Simulations", + }, + ) + + +@router.get( + "/ui/reporting", + response_class=HTMLResponse, + include_in_schema=False, + name="ui.reporting", +) +def reporting_dashboard( + request: Request, + _: User = Depends(require_any_role(*READ_ROLES)), +) -> HTMLResponse: + return templates.TemplateResponse( + request, + "reporting.html", + { + "title": "Reporting", + }, + ) + + +@router.get( + "/ui/settings", + response_class=HTMLResponse, + include_in_schema=False, + name="ui.settings", +) +def settings_page( + request: Request, + _: User = Depends(require_any_role(*READ_ROLES)), +) -> HTMLResponse: + return templates.TemplateResponse( + request, + "settings.html", + { + "title": "Settings", + }, + ) + + +@router.get( + "/theme-settings", + response_class=HTMLResponse, + include_in_schema=False, + name="ui.theme_settings", +) +def theme_settings_page( + request: Request, + _: User = Depends(require_any_role(*READ_ROLES)), +) -> HTMLResponse: + return templates.TemplateResponse( + request, + "theme_settings.html", + { + "title": "Theme Settings", + }, + ) + + +@router.get( + "/ui/currencies", + response_class=HTMLResponse, + include_in_schema=False, + name="ui.currencies", +) +def currencies_page( + request: Request, + _: User = Depends(require_roles(*MANAGE_ROLES)), +) -> HTMLResponse: + return templates.TemplateResponse( + request, + "currencies.html", + { + "title": "Currency Management", + }, + ) diff --git a/services/reporting.py b/services/reporting.py index 8445edc..a708b02 100644 --- a/services/reporting.py +++ b/services/reporting.py @@ -8,6 +8,9 @@ import math from typing import Mapping, Sequence from urllib.parse import urlencode +import plotly.graph_objects as go +import plotly.io as pio + from fastapi import Request from models import FinancialCategory, Project, Scenario @@ -515,6 +518,7 @@ class ReportingService: "label": "Download JSON", } ], + "chart_data": self._generate_npv_comparison_chart(reports), } def build_scenario_comparison_context( @@ -611,8 +615,64 @@ class ReportingService: "label": "Download JSON", } ], + "chart_data": self._generate_distribution_histogram(report.monte_carlo) if report.monte_carlo else "{}", } + def _generate_npv_comparison_chart(self, reports: Sequence[ScenarioReport]) -> str: + """Generate Plotly chart JSON for NPV comparison across scenarios.""" + scenario_names = [] + npv_values = [] + + for report in reports: + scenario_names.append(report.scenario.name) + npv_values.append(report.deterministic.npv or 0) + + fig = go.Figure(data=[ + go.Bar( + x=scenario_names, + y=npv_values, + name='NPV', + marker_color='lightblue' + ) + ]) + + fig.update_layout( + title="NPV Comparison Across Scenarios", + xaxis_title="Scenario", + yaxis_title="NPV", + showlegend=False + ) + + return pio.to_json(fig) or "{}" + + def _generate_distribution_histogram(self, monte_carlo: ScenarioMonteCarloResult) -> str: + """Generate Plotly histogram for Monte Carlo distribution.""" + if not monte_carlo.available or not monte_carlo.result or not monte_carlo.result.samples: + return "{}" + + # Get NPV samples + npv_samples = monte_carlo.result.samples.get(SimulationMetric.NPV, []) + if len(npv_samples) == 0: + return "{}" + + fig = go.Figure(data=[ + go.Histogram( + x=npv_samples, + nbinsx=50, + name='NPV Distribution', + marker_color='lightgreen' + ) + ]) + + fig.update_layout( + title="Monte Carlo NPV Distribution", + xaxis_title="NPV", + yaxis_title="Frequency", + showlegend=False + ) + + return pio.to_json(fig) or "{}" + def _build_cash_flows(scenario: Scenario) -> tuple[list[CashFlow], ScenarioFinancialTotals]: cash_flows: list[CashFlow] = [] diff --git a/static/css/main.css b/static/css/main.css index 5a174c4..4277eff 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -117,6 +117,16 @@ a { margin-top: 3rem; } +.chart-container { + width: 100%; + height: 400px; + background: rgba(15, 20, 27, 0.8); + border-radius: var(--radius-sm); + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + margin-bottom: 1rem; +} + .section-header { margin-bottom: 1.25rem; } diff --git a/static/js/navigation.js b/static/js/navigation.js index 62174e9..f1113fc 100644 --- a/static/js/navigation.js +++ b/static/js/navigation.js @@ -7,12 +7,12 @@ document.addEventListener("DOMContentLoaded", function () { // Define the navigation order (main pages) const navPages = [ - "/", - "/projects/ui", - "/imports/ui", - "/ui/simulations", - "/ui/reporting", - "/ui/settings", + window.NAVIGATION_URLS.dashboard, + window.NAVIGATION_URLS.projects, + window.NAVIGATION_URLS.imports, + window.NAVIGATION_URLS.simulations, + window.NAVIGATION_URLS.reporting, + window.NAVIGATION_URLS.settings, ]; const currentPath = window.location.pathname; diff --git a/templates/base.html b/templates/base.html index 99e0561..ae3f5d8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,7 +5,7 @@