Refactor and enhance CalMiner application
- Updated README.md to reflect new features and usage instructions. - Removed deprecated Dashboard.html component and integrated dashboard functionality directly into the main application. - Revised architecture documentation for clarity and added module map and request flow diagrams. - Enhanced maintenance model to include equipment association and cost tracking. - Updated requirements.txt to include new dependencies (httpx, pandas, numpy). - Improved consumption, maintenance, production, and reporting routes with better validation and response handling. - Added unit tests for maintenance and production routes, ensuring proper CRUD operations and validation. - Enhanced reporting service to calculate and return detailed summary statistics. - Redesigned Dashboard.html for improved user experience and integrated Chart.js for visualizing simulation results.
This commit is contained in:
@@ -1,25 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>CalMiner Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 2rem;
|
||||
background-color: #f4f5f7;
|
||||
color: #1f2933;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simulation Results Dashboard</h1>
|
||||
<div id="report-summary"></div>
|
||||
<div class="summary-card">
|
||||
<h2>Summary Statistics</h2>
|
||||
<div id="summary-grid" class="summary-grid"></div>
|
||||
<p id="error-message" hidden></p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h2>Sample Results Input</h2>
|
||||
<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"
|
||||
style="width: 100%; font-family: monospace"
|
||||
></textarea>
|
||||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem">
|
||||
<button id="load-sample" type="button">Load Sample Data</button>
|
||||
<button id="refresh-dashboard" type="button">Refresh Dashboard</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart-container">
|
||||
<h2>Result Distribution</h2>
|
||||
<canvas id="summary-chart" height="120"></canvas>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// TODO: fetch summary report and render charts
|
||||
async function loadReport() {
|
||||
const response = await fetch('/api/reporting/summary', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([])
|
||||
});
|
||||
const data = await response.json();
|
||||
document.getElementById('report-summary').innerText = JSON.stringify(data);
|
||||
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: "percentile_10", label: "10th Percentile" },
|
||||
{ key: "percentile_90", label: "90th Percentile" },
|
||||
];
|
||||
|
||||
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");
|
||||
}
|
||||
loadReport();
|
||||
|
||||
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 value = summary[key] ?? 0;
|
||||
const metric = document.createElement("div");
|
||||
metric.className = "metric";
|
||||
metric.innerHTML = `
|
||||
<div class="metric-label">${label}</div>
|
||||
<div class="metric-value">${value.toFixed(2)}</div>
|
||||
`;
|
||||
grid.appendChild(metric);
|
||||
});
|
||||
}
|
||||
|
||||
let chartInstance = null;
|
||||
|
||||
function renderChart(summary) {
|
||||
const ctx = document.getElementById("summary-chart").getContext("2d");
|
||||
const dataPoints = [
|
||||
summary.min,
|
||||
summary.percentile_10,
|
||||
summary.median,
|
||||
summary.mean,
|
||||
summary.percentile_90,
|
||||
summary.max,
|
||||
].map((value) => Number(value ?? 0));
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: ["Min", "P10", "Median", "Mean", "P90", "Max"],
|
||||
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 },
|
||||
},
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user