feat: Add main CSS styles for the application
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
This commit is contained in:
@@ -1,277 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>CalMiner Dashboard</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 2rem;
|
||||
background-color: #f4f5f7;
|
||||
color: #1f2933;
|
||||
{% 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");
|
||||
}
|
||||
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 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>
|
||||
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%)",
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: 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);
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
const errorElement = document.getElementById("error-message");
|
||||
errorElement.textContent = message;
|
||||
errorElement.hidden = false;
|
||||
}
|
||||
document.getElementById("results-input").value = sampleData;
|
||||
}
|
||||
|
||||
function attachHandlers() {
|
||||
const loadSampleButton = document.getElementById("load-sample");
|
||||
const refreshButton = document.getElementById("refresh-dashboard");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
initializeDashboard();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,50 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Process Parameters Input</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Enter Parameters for a Scenario</h1>
|
||||
<form id="parameter-form">
|
||||
<label
|
||||
>Scenario ID:
|
||||
<input
|
||||
type="number"
|
||||
name="scenario_id"
|
||||
id="scenario_id"
|
||||
required /></label
|
||||
><br />
|
||||
<label>Name: <input type="text" name="name" id="name" required /></label
|
||||
><br />
|
||||
<label
|
||||
>Value:
|
||||
<input
|
||||
type="number"
|
||||
name="value"
|
||||
id="value"
|
||||
step="any"
|
||||
required /></label
|
||||
><br />
|
||||
<button type="submit">Add Parameter</button>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
document
|
||||
.getElementById("parameter-form")
|
||||
.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const scenario_id = document.getElementById("scenario_id").value;
|
||||
const name = document.getElementById("name").value;
|
||||
const value = parseFloat(document.getElementById("value").value);
|
||||
const resp = await fetch("/api/parameters/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scenario_id, name, value }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
document.getElementById("result").innerText = JSON.stringify(data);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %} {% block title %}Process Parameters · CalMiner{%
|
||||
endblock %} {% block content %}
|
||||
<section class="panel">
|
||||
<h2>Scenario Parameters</h2>
|
||||
{% if scenarios %}
|
||||
<form id="parameter-form" class="form-grid">
|
||||
<label>
|
||||
<span>Scenario</span>
|
||||
<select name="scenario_id" id="scenario_id">
|
||||
{% for scenario in scenarios %}
|
||||
<option value="{{ scenario.id }}">{{ scenario.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" id="name" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Value</span>
|
||||
<input type="number" name="value" id="value" step="any" required />
|
||||
</label>
|
||||
<button type="submit" class="btn primary">Add Parameter</button>
|
||||
</form>
|
||||
<p id="parameter-feedback" class="feedback" role="status"></p>
|
||||
<div class="table-container">
|
||||
<table id="parameter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Parameter</th>
|
||||
<th scope="col">Value</th>
|
||||
<th scope="col">Distribution</th>
|
||||
<th scope="col">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="parameter-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">
|
||||
No scenarios available. Create a scenario before adding parameters.
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %} {% block scripts %} {{ super() }}
|
||||
<script id="parameters-data" type="application/json">
|
||||
{{ parameters_by_scenario | tojson | safe }}
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const dataElement = document.getElementById("parameters-data");
|
||||
const parsedData = dataElement
|
||||
? JSON.parse(dataElement.textContent || "{}")
|
||||
: {};
|
||||
const parametersByScenario =
|
||||
parsedData && typeof parsedData === "object" ? parsedData : {};
|
||||
const form = document.getElementById("parameter-form");
|
||||
const scenarioSelect = /** @type {HTMLSelectElement | null} */ (
|
||||
document.getElementById("scenario_id")
|
||||
);
|
||||
const nameInput = /** @type {HTMLInputElement | null} */ (
|
||||
document.getElementById("name")
|
||||
);
|
||||
const valueInput = /** @type {HTMLInputElement | null} */ (
|
||||
document.getElementById("value")
|
||||
);
|
||||
const feedback = document.getElementById("parameter-feedback");
|
||||
const tableBody = document.getElementById("parameter-table-body");
|
||||
|
||||
const setFeedback = (message, variant) => {
|
||||
if (!feedback) {
|
||||
return;
|
||||
}
|
||||
feedback.textContent = message;
|
||||
feedback.classList.remove("success", "error");
|
||||
if (variant) {
|
||||
feedback.classList.add(variant);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTable = (scenarioId) => {
|
||||
if (!tableBody) {
|
||||
return;
|
||||
}
|
||||
tableBody.innerHTML = "";
|
||||
const rows = parametersByScenario[String(scenarioId)] || [];
|
||||
if (!rows.length) {
|
||||
const emptyRow = document.createElement("tr");
|
||||
emptyRow.id = "parameter-empty-state";
|
||||
emptyRow.innerHTML =
|
||||
'<td colspan="4">No parameters recorded for this scenario yet.</td>';
|
||||
tableBody.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
rows.forEach((row) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${row.name}</td>
|
||||
<td>${row.value}</td>
|
||||
<td>${row.distribution_type ?? "—"}</td>
|
||||
<td>${
|
||||
row.distribution_parameters
|
||||
? JSON.stringify(row.distribution_parameters)
|
||||
: "—"
|
||||
}</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
};
|
||||
|
||||
if (scenarioSelect) {
|
||||
renderTable(scenarioSelect.value);
|
||||
scenarioSelect.addEventListener("change", () =>
|
||||
renderTable(scenarioSelect.value)
|
||||
);
|
||||
}
|
||||
|
||||
if (!form || !scenarioSelect || !nameInput || !valueInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const scenarioId = scenarioSelect.value;
|
||||
const payload = {
|
||||
scenario_id: Number(scenarioId),
|
||||
name: nameInput.value.trim(),
|
||||
value: Number(valueInput.value),
|
||||
};
|
||||
|
||||
if (!payload.name) {
|
||||
setFeedback("Parameter name is required.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(payload.value)) {
|
||||
setFeedback("Enter a numeric value.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/parameters/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
setFeedback(`Error saving parameter: ${errorText}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const scenarioKey = String(scenarioId);
|
||||
parametersByScenario[scenarioKey] =
|
||||
parametersByScenario[scenarioKey] || [];
|
||||
parametersByScenario[scenarioKey].push(data);
|
||||
|
||||
form.reset();
|
||||
scenarioSelect.value = scenarioKey;
|
||||
renderTable(scenarioKey);
|
||||
nameInput.focus();
|
||||
setFeedback("Parameter saved.", "success");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,36 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Scenario Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Create a New Scenario</h1>
|
||||
<form id="scenario-form">
|
||||
<label>Name: <input type="text" name="name" id="name" required /></label
|
||||
><br />
|
||||
<label
|
||||
>Description:
|
||||
<input type="text" name="description" id="description" /></label
|
||||
><br />
|
||||
<button type="submit">Create Scenario</button>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
<script>
|
||||
document
|
||||
.getElementById("scenario-form")
|
||||
.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById("name").value;
|
||||
const description = document.getElementById("description").value;
|
||||
const resp = await fetch("/api/scenarios/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
document.getElementById("result").innerText = JSON.stringify(data);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %} {% block title %}Scenario Management · CalMiner{%
|
||||
endblock %} {% block content %}
|
||||
<section class="panel">
|
||||
<h2>Create a New Scenario</h2>
|
||||
<form id="scenario-form" class="form-grid">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="name" id="name" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Description</span>
|
||||
<input type="text" name="description" id="description" />
|
||||
</label>
|
||||
<button type="submit" class="btn primary">Create Scenario</button>
|
||||
</form>
|
||||
<div class="table-container">
|
||||
{% if scenarios %}
|
||||
<table id="scenario-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scenario-table-body">
|
||||
{% for scenario in scenarios %}
|
||||
<tr data-scenario-id="{{ scenario.id }}">
|
||||
<td>{{ scenario.name }}</td>
|
||||
<td>{{ scenario.description or "—" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p id="empty-state" class="empty-state">
|
||||
No scenarios yet. Create one to get started.
|
||||
</p>
|
||||
<table id="scenario-table" class="hidden" aria-hidden="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scenario-table-body"></tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %} {% block scripts %} {{ super() }}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.getElementById("scenario-form");
|
||||
const nameInput = /** @type {HTMLInputElement} */ (
|
||||
document.getElementById("name")
|
||||
);
|
||||
const descriptionInput = /** @type {HTMLInputElement} */ (
|
||||
document.getElementById("description")
|
||||
);
|
||||
const table = document.getElementById("scenario-table");
|
||||
const tableBody = document.getElementById("scenario-table-body");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const payload = {
|
||||
name: nameInput.value.trim(),
|
||||
description: descriptionInput.value.trim() || null,
|
||||
};
|
||||
|
||||
if (!payload.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/scenarios/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Scenario creation failed", errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.scenarioId = String(data.id);
|
||||
row.innerHTML = `
|
||||
<td>${data.name}</td>
|
||||
<td>${data.description ?? "—"}</td>
|
||||
`;
|
||||
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.classList.remove("hidden");
|
||||
table.removeAttribute("aria-hidden");
|
||||
}
|
||||
|
||||
if (tableBody) {
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
|
||||
form.reset();
|
||||
nameInput.focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
17
templates/base.html
Normal file
17
templates/base.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}CalMiner{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css" />
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% include "partials/base_header.html" %}
|
||||
<main id="content" class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% include "partials/base_footer.html" %} {% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
8
templates/consumption.html
Normal file
8
templates/consumption.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Consumption · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Consumption Tracking</h2>
|
||||
<p>Placeholder for tracking scenario consumption metrics. Integration with APIs coming soon.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/costs.html
Normal file
8
templates/costs.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Costs · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Costs</h2>
|
||||
<p>This view will surface CAPEX and OPEX entries tied to scenarios. API wiring pending.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/equipment.html
Normal file
8
templates/equipment.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Equipment · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Equipment Inventory</h2>
|
||||
<p>Placeholder for equipment CRUD interface. To be wired to `/api/equipment/` routes.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/maintenance.html
Normal file
8
templates/maintenance.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Maintenance · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Maintenance Log</h2>
|
||||
<p>Placeholder for maintenance records management. Future work will surface CRUD flows tied to `/api/maintenance/`.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
5
templates/partials/base_footer.html
Normal file
5
templates/partials/base_footer.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>© {{ current_year }} CalMiner. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
17
templates/partials/base_header.html
Normal file
17
templates/partials/base_header.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<h1 class="site-title">CalMiner</h1>
|
||||
<nav class="site-nav" aria-label="Primary navigation">
|
||||
<a href="/ui/dashboard">Dashboard</a>
|
||||
<a href="/ui/scenarios">Scenarios</a>
|
||||
<a href="/ui/parameters">Parameters</a>
|
||||
<a href="/ui/costs">Costs</a>
|
||||
<a href="/ui/consumption">Consumption</a>
|
||||
<a href="/ui/production">Production</a>
|
||||
<a href="/ui/equipment">Equipment</a>
|
||||
<a href="/ui/maintenance">Maintenance</a>
|
||||
<a href="/ui/simulations">Simulations</a>
|
||||
<a href="/ui/reporting">Reporting</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
8
templates/production.html
Normal file
8
templates/production.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Production · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Production Output</h2>
|
||||
<p>Placeholder for production metrics per scenario. Hook up to `/api/production/` endpoints.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/reporting.html
Normal file
8
templates/reporting.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reporting · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Reporting</h2>
|
||||
<p>Placeholder for aggregated KPI views connected to `/api/reporting/` endpoints.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/simulations.html
Normal file
8
templates/simulations.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Simulations · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Monte Carlo Simulations</h2>
|
||||
<p>Placeholder for running simulations and reviewing outputs. Target integration: `/api/simulations/run`.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user