feat(calibration): finalize Step 2 Risk & Stops with inline PDF reports and visual validation
This commit is contained in:
@@ -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)
|
||||
# ==================================================
|
||||
|
||||
@@ -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}"'
|
||||
# },
|
||||
# )
|
||||
|
||||
@@ -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 = "";
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user