feat(calibration): finalize Step 2 Risk & Stops with inline PDF reports and visual validation

This commit is contained in:
DaM
2026-02-13 20:56:34 +01:00
parent 44667df3dd
commit f4f4e8e5be
20 changed files with 184 additions and 1925 deletions

View File

@@ -68,6 +68,18 @@ def create_app() -> FastAPI:
name="static",
)
# -------------------------
# Reports folder (public access)
# -------------------------
reports_root = PROJECT_ROOT / "reports"
reports_root.mkdir(exist_ok=True)
app.mount(
"/reports",
StaticFiles(directory=str(reports_root)),
name="reports",
)
# ==================================================
# ROUTES — UI ONLY (TEMPORAL)
# ==================================================

View File

@@ -3,9 +3,10 @@
import logging
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from pathlib import Path
import uuid
import re
from src.data.storage import StorageManager
from src.calibration.risk_inspector import (
@@ -261,15 +262,29 @@ def generate_risk_report(
results = result
# ---------------------------------------------
# Generate file
# ---------------------------------------------
# ============================================================
# BUILD SAFE OUTPUT PATH (OUTSIDE src/)
# ============================================================
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
# Project root (3 niveles arriba desde router)
project_root = Path(__file__).resolve().parents[5]
filename = f"risk_report_{uuid.uuid4().hex}.pdf"
output_path = reports_dir / filename
# Reports folder outside src
reports_dir = project_root / "reports" / "risk"
reports_dir.mkdir(parents=True, exist_ok=True)
# Sanitize symbol for filesystem
safe_symbol = re.sub(r"[^a-zA-Z0-9_-]", "_", payload.symbol)
# Unique filename
file_id = uuid.uuid4().hex
filename = f"risk_report_{safe_symbol}_{payload.timeframe}_{file_id}.pdf"
symbol_dir = reports_dir / safe_symbol
symbol_dir.mkdir(exist_ok=True)
output_path = symbol_dir / filename
generate_risk_report_pdf(
output_path=output_path,
@@ -278,8 +293,24 @@ def generate_risk_report(
results=results,
)
return FileResponse(
path=str(output_path),
media_type="application/pdf",
filename=filename,
# ---------------------------------------------
# Create public URL
# ---------------------------------------------
public_url = f"/reports/risk/{safe_symbol}/{filename}"
return JSONResponse(
content={
"status": "ok",
"url": public_url,
}
)
# return FileResponse(
# path=str(output_path),
# media_type="application/pdf",
# filename=output_path.name,
# headers={
# "Content-Disposition": f'inline; filename="{output_path.name}"'
# },
# )

View File

@@ -212,7 +212,7 @@ async function validateCalibrationRisk() {
async function generateRiskReport() {
console.log("[calibration_risk] generateRiskReport() START");
const payload = buildRiskPayload(); // reutiliza la misma función que validate
const payload = buildRiskPayload();
const res = await fetch("/api/v2/calibration/risk/report", {
method: "POST",
@@ -220,25 +220,23 @@ async function generateRiskReport() {
body: JSON.stringify(payload),
});
if (!res.ok) {
console.error("Failed to generate report");
return;
const data = await res.json();
if (data.url) {
// Mostrar visor
const viewer = document.getElementById("pdf_viewer_section");
const frame = document.getElementById("pdf_frame");
frame.src = data.url;
viewer.classList.remove("d-none");
// Scroll suave hacia el visor
viewer.scrollIntoView({ behavior: "smooth" });
} else {
alert("Failed to generate report");
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "risk_report.pdf";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
// =================================================
// WIZARD NAVIGATION
// =================================================
@@ -804,6 +802,13 @@ document.addEventListener("DOMContentLoaded", () => {
document
.getElementById("generate_report_btn")
?.addEventListener("click", generateRiskReport);
document
.getElementById("close_pdf_btn")
?.addEventListener("click", () => {
document.getElementById("pdf_viewer_section").classList.add("d-none");
document.getElementById("pdf_frame").src = "";
});
});

View File

@@ -238,6 +238,27 @@
<div id="stop_distance_plot" style="height: 360px;"></div>
</div>
<!-- ========================= -->
<!-- PDF REPORT VIEWER -->
<!-- ========================= -->
<div id="pdf_viewer_section" class="mt-5 d-none">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="card-title mb-0">Risk Report Preview</h4>
<button id="close_pdf_btn" class="btn btn-sm btn-outline-secondary">
Close
</button>
</div>
<iframe
id="pdf_frame"
style="width: 100%; height: 800px; border: none;"
></iframe>
</div>
</div>
</div>
</div>
<!-- Plotly -->
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>