feat: Refactor Dashboard template to extend base layout and improve structure feat: Enhance Parameter Input template with improved layout and feedback mechanisms feat: Update Scenario Form template to utilize base layout and improve user experience feat: Create base layout template for consistent styling across pages feat: Add Consumption, Costs, Equipment, Maintenance, Production, Reporting, and Simulations templates with placeholders for future functionality feat: Implement base header and footer partials for consistent navigation and footer across the application
262 lines
7.2 KiB
HTML
262 lines
7.2 KiB
HTML
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
|
|
block head_extra %} {{ super() }}
|
|
<style>
|
|
.summary-card {
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
padding: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.summary-grid {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
}
|
|
.metric {
|
|
text-align: center;
|
|
}
|
|
.metric-label {
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #52606d;
|
|
}
|
|
.metric-value {
|
|
font-size: 1.4rem;
|
|
font-weight: bold;
|
|
margin-top: 0.4rem;
|
|
}
|
|
#chart-container {
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
padding: 1.5rem;
|
|
}
|
|
#error-message {
|
|
color: #b91d47;
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|
|
{% endblock %} {% block content %}
|
|
<h2>Simulation Results Dashboard</h2>
|
|
<div class="summary-card">
|
|
<h3>Summary Statistics</h3>
|
|
<div id="summary-grid" class="summary-grid"></div>
|
|
<p id="error-message" hidden></p>
|
|
</div>
|
|
<div class="summary-card">
|
|
<h3>Sample Results Input</h3>
|
|
<p>
|
|
Provide simulation outputs as JSON (array of objects containing the
|
|
<code>result</code> field) and refresh the dashboard to preview metrics.
|
|
</p>
|
|
<textarea id="results-input" rows="6" class="monospace-input"></textarea>
|
|
<div class="button-row">
|
|
<button id="load-sample" type="button" class="btn">Load Sample Data</button>
|
|
<button id="refresh-dashboard" type="button" class="btn primary">
|
|
Refresh Dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="chart-container">
|
|
<h3>Result Distribution</h3>
|
|
<canvas id="summary-chart" height="120"></canvas>
|
|
</div>
|
|
{% endblock %} {% block scripts %} {{ super() }}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
const SUMMARY_FIELDS = [
|
|
{ key: "mean", label: "Mean" },
|
|
{ key: "median", label: "Median" },
|
|
{ key: "min", label: "Min" },
|
|
{ key: "max", label: "Max" },
|
|
{ key: "std_dev", label: "Std Dev" },
|
|
{ key: "variance", label: "Variance" },
|
|
{ key: "percentile_5", label: "5th Percentile" },
|
|
{ key: "percentile_10", label: "10th Percentile" },
|
|
{ key: "percentile_90", label: "90th Percentile" },
|
|
{ key: "percentile_95", label: "95th Percentile" },
|
|
{ key: "value_at_risk_95", label: "VaR (95%)" },
|
|
{
|
|
key: "expected_shortfall_95",
|
|
label: "Expected Shortfall (95%)",
|
|
},
|
|
];
|
|
|
|
async function fetchSummary(results) {
|
|
const response = await fetch("/api/reporting/summary", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(results),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const message = await response.json();
|
|
throw new Error(message.detail || "Failed to retrieve summary");
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
function getResultsFromInput() {
|
|
const textarea = document.getElementById("results-input");
|
|
try {
|
|
const parsed = JSON.parse(textarea.value || "[]");
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error("Input must be a JSON array");
|
|
}
|
|
return parsed;
|
|
} catch (error) {
|
|
throw new Error(`Invalid JSON input: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function renderSummary(summary) {
|
|
const grid = document.getElementById("summary-grid");
|
|
grid.innerHTML = "";
|
|
SUMMARY_FIELDS.forEach(({ key, label }) => {
|
|
const rawValue = summary[key];
|
|
const numericValue = Number(rawValue);
|
|
const display = Number.isFinite(numericValue)
|
|
? numericValue.toFixed(2)
|
|
: "—";
|
|
const metric = document.createElement("div");
|
|
metric.className = "metric";
|
|
metric.innerHTML = `
|
|
<div class="metric-label">${label}</div>
|
|
<div class="metric-value">${display}</div>
|
|
`;
|
|
grid.appendChild(metric);
|
|
});
|
|
}
|
|
|
|
let chartInstance = null;
|
|
|
|
function renderChart(summary) {
|
|
const ctx = document.getElementById("summary-chart").getContext("2d");
|
|
const percentilePoints = [
|
|
{ label: "Min", value: summary.min },
|
|
{ label: "P5", value: summary.percentile_5 },
|
|
{ label: "P10", value: summary.percentile_10 },
|
|
{ label: "Median", value: summary.median },
|
|
{ label: "Mean", value: summary.mean },
|
|
{ label: "P90", value: summary.percentile_90 },
|
|
{ label: "P95", value: summary.percentile_95 },
|
|
{ label: "Max", value: summary.max },
|
|
];
|
|
|
|
const labels = percentilePoints.map((point) => point.label);
|
|
const dataPoints = percentilePoints.map((point) =>
|
|
Number(point.value ?? 0)
|
|
);
|
|
|
|
const tailRiskLines = [
|
|
{ label: "VaR (95%)", value: summary.value_at_risk_95 },
|
|
{ label: "ES (95%)", value: summary.expected_shortfall_95 },
|
|
]
|
|
.map(({ label, value }) => {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) {
|
|
return null;
|
|
}
|
|
return `${label}: ${numeric.toFixed(2)}`;
|
|
})
|
|
.filter((line) => line !== null);
|
|
|
|
if (chartInstance) {
|
|
chartInstance.destroy();
|
|
}
|
|
|
|
chartInstance = new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: "Result Summary",
|
|
data: dataPoints,
|
|
borderColor: "#2563eb",
|
|
backgroundColor: "rgba(37, 99, 235, 0.2)",
|
|
tension: 0.3,
|
|
fill: true,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
afterBody: () => tailRiskLines,
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorElement = document.getElementById("error-message");
|
|
errorElement.textContent = message;
|
|
errorElement.hidden = false;
|
|
}
|
|
|
|
function attachHandlers() {
|
|
const loadSampleButton = document.getElementById("load-sample");
|
|
const refreshButton = document.getElementById("refresh-dashboard");
|
|
|
|
const sampleData = JSON.stringify(
|
|
[
|
|
{ result: 18.2 },
|
|
{ result: 22.1 },
|
|
{ result: 30.4 },
|
|
{ result: 25.7 },
|
|
{ result: 28.3 },
|
|
],
|
|
null,
|
|
2
|
|
);
|
|
|
|
loadSampleButton.addEventListener("click", () => {
|
|
document.getElementById("results-input").value = sampleData;
|
|
});
|
|
|
|
refreshButton.addEventListener("click", async () => {
|
|
try {
|
|
const results = getResultsFromInput();
|
|
const summary = await fetchSummary(results);
|
|
renderSummary(summary);
|
|
renderChart(summary);
|
|
document.getElementById("error-message").hidden = true;
|
|
} catch (error) {
|
|
console.error(error);
|
|
showError(error.message);
|
|
}
|
|
});
|
|
|
|
document.getElementById("results-input").value = sampleData;
|
|
}
|
|
|
|
async function initializeDashboard() {
|
|
try {
|
|
attachHandlers();
|
|
const initialResults = getResultsFromInput();
|
|
const summary = await fetchSummary(initialResults);
|
|
renderSummary(summary);
|
|
renderChart(summary);
|
|
} catch (error) {
|
|
console.error(error);
|
|
showError(error.message);
|
|
}
|
|
}
|
|
|
|
initializeDashboard();
|
|
</script>
|
|
{% endblock %}
|