Refactor templates to externalize JavaScript: Moved inline scripts to separate JS files and added JSON data attributes for better maintainability and performance. Updated consumption, costs, equipment, maintenance, production, reporting, and simulations templates accordingly.

This commit is contained in:
2025-10-21 07:20:02 +02:00
parent 5a84445e90
commit 18f4ae7278
21 changed files with 1963 additions and 1986 deletions

View File

@@ -21,6 +21,7 @@ A range of features are implemented to support these functionalities.
- **Unified UI Shell**: Server-rendered templates extend a shared base layout with a persistent left sidebar linking scenarios, parameters, costs, consumption, production, equipment, maintenance, simulations, and reporting views. - **Unified UI Shell**: Server-rendered templates extend a shared base layout with a persistent left sidebar linking scenarios, parameters, costs, consumption, production, equipment, maintenance, simulations, and reporting views.
- **Operations Overview Dashboard**: The root route (`/`) surfaces cross-scenario KPIs, charts, and maintenance reminders with a one-click refresh backed by aggregated loaders. - **Operations Overview Dashboard**: The root route (`/`) surfaces cross-scenario KPIs, charts, and maintenance reminders with a one-click refresh backed by aggregated loaders.
- **Theming Tokens**: Shared CSS variables in `static/css/main.css` centralize the UI color palette for consistent styling and rapid theme tweaks. - **Theming Tokens**: Shared CSS variables in `static/css/main.css` centralize the UI color palette for consistent styling and rapid theme tweaks.
- **Modular Frontend Scripts**: Page-specific interactions now live in `static/js/` modules, keeping templates lean while enabling browser caching and reuse.
- **Monte Carlo Simulation (in progress)**: Services and routes are scaffolded for future stochastic analysis. - **Monte Carlo Simulation (in progress)**: Services and routes are scaffolded for future stochastic analysis.
## Architecture ## Architecture

156
static/js/consumption.js Normal file
View File

@@ -0,0 +1,156 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("consumption-data");
let data = { scenarios: [], consumption: {} };
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
data = {
scenarios: Array.isArray(parsed.scenarios) ? parsed.scenarios : [],
consumption:
parsed.consumption && typeof parsed.consumption === "object"
? parsed.consumption
: {},
};
}
} catch (error) {
console.error("Unable to parse consumption data", error);
}
}
const consumptionByScenario = data.consumption;
const filterSelect = document.getElementById("consumption-scenario-filter");
const tableWrapper = document.getElementById("consumption-table-wrapper");
const tableBody = document.getElementById("consumption-table-body");
const emptyState = document.getElementById("consumption-empty");
const form = document.getElementById("consumption-form");
const feedbackEl = document.getElementById("consumption-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const renderConsumptionRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = consumptionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No consumption records for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review its consumption records.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderConsumptionRows(value);
});
}
const submitConsumption = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/consumption/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add consumption record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(consumptionByScenario[mapKey])) {
consumptionByScenario[mapKey] = [];
}
consumptionByScenario[mapKey].push(result);
form.reset();
showFeedback("Consumption record saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderConsumptionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitConsumption);
}
if (filterSelect && filterSelect.value) {
renderConsumptionRows(filterSelect.value);
}
});

240
static/js/costs.js Normal file
View File

@@ -0,0 +1,240 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("costs-payload");
let capexByScenario = {};
let opexByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.capex && typeof parsed.capex === "object") {
capexByScenario = parsed.capex;
}
if (parsed.opex && typeof parsed.opex === "object") {
opexByScenario = parsed.opex;
}
}
} catch (error) {
console.error("Unable to parse cost data", error);
}
}
const filterSelect = document.getElementById("costs-scenario-filter");
const costsEmptyState = document.getElementById("costs-empty");
const costsDataWrapper = document.getElementById("costs-data");
const capexTableBody = document.getElementById("capex-table-body");
const opexTableBody = document.getElementById("opex-table-body");
const capexEmpty = document.getElementById("capex-empty");
const opexEmpty = document.getElementById("opex-empty");
const capexTotal = document.getElementById("capex-total");
const opexTotal = document.getElementById("opex-total");
const capexForm = document.getElementById("capex-form");
const opexForm = document.getElementById("opex-form");
const capexFeedback = document.getElementById("capex-feedback");
const opexFeedback = document.getElementById("opex-feedback");
const capexFormScenario = document.getElementById("capex-form-scenario");
const opexFormScenario = document.getElementById("opex-form-scenario");
const showFeedback = (element, message, type = "success") => {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
};
const hideFeedback = (element) => {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const sumAmount = (records) =>
records.reduce((total, record) => total + Number(record.amount || 0), 0);
const renderCostTables = (scenarioId) => {
if (
!capexTableBody ||
!opexTableBody ||
!capexEmpty ||
!opexEmpty ||
!capexTotal ||
!opexTotal
) {
return;
}
const capexRecords = capexByScenario[String(scenarioId)] || [];
const opexRecords = opexByScenario[String(scenarioId)] || [];
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
if (!capexRecords.length) {
capexEmpty.classList.remove("hidden");
} else {
capexEmpty.classList.add("hidden");
capexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
capexTableBody.appendChild(row);
});
}
if (!opexRecords.length) {
opexEmpty.classList.remove("hidden");
} else {
opexEmpty.classList.add("hidden");
opexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
opexTableBody.appendChild(row);
});
}
capexTotal.textContent = formatAmount(sumAmount(capexRecords));
opexTotal.textContent = formatAmount(sumAmount(opexRecords));
};
const toggleCostView = (show) => {
if (
!costsEmptyState ||
!costsDataWrapper ||
!capexTableBody ||
!opexTableBody
) {
return;
}
if (show) {
costsEmptyState.classList.add("hidden");
costsDataWrapper.classList.remove("hidden");
} else {
costsEmptyState.classList.remove("hidden");
costsDataWrapper.classList.add("hidden");
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
if (capexTotal) {
capexTotal.textContent = "—";
}
if (opexTotal) {
opexTotal.textContent = "—";
}
if (capexEmpty) {
capexEmpty.classList.add("hidden");
}
if (opexEmpty) {
opexEmpty.classList.add("hidden");
}
}
};
const syncFormSelections = (value) => {
if (capexFormScenario) {
capexFormScenario.value = value || "";
}
if (opexFormScenario) {
opexFormScenario.value = value || "";
}
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
toggleCostView(false);
syncFormSelections("");
return;
}
toggleCostView(true);
renderCostTables(value);
syncFormSelections(value);
});
}
const submitCostEntry = async (event, targetUrl, storageMap, feedbackEl) => {
event.preventDefault();
hideFeedback(feedbackEl);
const formData = new FormData(event.target);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
if (!payload.scenario_id) {
showFeedback(feedbackEl, "Select a scenario before submitting.", "error");
return;
}
try {
const response = await fetch(targetUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to save cost entry.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(storageMap[mapKey])) {
storageMap[mapKey] = [];
}
storageMap[mapKey].push(result);
event.target.reset();
showFeedback(feedbackEl, "Entry saved successfully.", "success");
if (filterSelect && filterSelect.value === mapKey) {
renderCostTables(mapKey);
}
} catch (error) {
showFeedback(
feedbackEl,
error.message || "An unexpected error occurred.",
"error"
);
}
};
if (capexForm) {
capexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/capex", capexByScenario, capexFeedback)
);
}
if (opexForm) {
opexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/opex", opexByScenario, opexFeedback)
);
}
if (filterSelect && filterSelect.value) {
toggleCostView(true);
renderCostTables(filterSelect.value);
syncFormSelections(filterSelect.value);
}
});

289
static/js/dashboard.js Normal file
View File

@@ -0,0 +1,289 @@
(() => {
const dataElement = document.getElementById("dashboard-data");
if (!dataElement) {
return;
}
let state = {};
try {
state = JSON.parse(dataElement.textContent || "{}");
} catch (error) {
console.error("Failed to parse dashboard data", error);
return;
}
const statusElement = document.getElementById("dashboard-status");
const summaryContainer = document.getElementById("summary-metrics");
const summaryEmpty = document.getElementById("summary-empty");
const scenarioTableBody = document.querySelector("#scenario-table tbody");
const scenarioEmpty = document.getElementById("scenario-table-empty");
const overallMetricsList = document.getElementById("overall-metrics");
const overallMetricsEmpty = document.getElementById("overall-metrics-empty");
const recentList = document.getElementById("recent-simulations");
const recentEmpty = document.getElementById("recent-simulations-empty");
const maintenanceList = document.getElementById("upcoming-maintenance");
const maintenanceEmpty = document.getElementById(
"upcoming-maintenance-empty"
);
const refreshButton = document.getElementById("refresh-dashboard");
const costChartCanvas = document.getElementById("cost-chart");
const costChartEmpty = document.getElementById("cost-chart-empty");
const activityChartCanvas = document.getElementById("activity-chart");
const activityChartEmpty = document.getElementById("activity-chart-empty");
let costChartInstance = null;
let activityChartInstance = null;
const setStatus = (message, variant = "success") => {
if (!statusElement) {
return;
}
if (!message) {
statusElement.hidden = true;
statusElement.textContent = "";
statusElement.classList.remove("success", "error");
return;
}
statusElement.textContent = message;
statusElement.hidden = false;
statusElement.classList.toggle("success", variant === "success");
statusElement.classList.toggle("error", variant !== "success");
};
const renderSummaryMetrics = () => {
if (!summaryContainer || !summaryEmpty) {
return;
}
summaryContainer.innerHTML = "";
const metrics = Array.isArray(state.summary_metrics)
? state.summary_metrics
: [];
metrics.forEach((metric) => {
const card = document.createElement("article");
card.className = "metric-card";
card.innerHTML = `
<span class="metric-label">${metric.label}</span>
<span class="metric-value">${metric.value}</span>
`;
summaryContainer.appendChild(card);
});
summaryEmpty.hidden = metrics.length > 0;
};
const renderScenarioTable = () => {
if (!scenarioTableBody || !scenarioEmpty) {
return;
}
scenarioTableBody.innerHTML = "";
const rows = Array.isArray(state.scenario_rows) ? state.scenario_rows : [];
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${row.scenario_name}</td>
<td>${row.parameter_display}</td>
<td>${row.equipment_display}</td>
<td>${row.capex_display}</td>
<td>${row.opex_display}</td>
<td>${row.production_display}</td>
<td>${row.consumption_display}</td>
<td>${row.maintenance_display}</td>
<td>${row.iterations_display}</td>
<td>${row.simulation_mean_display}</td>
`;
scenarioTableBody.appendChild(tr);
});
scenarioEmpty.hidden = rows.length > 0;
};
const renderOverallMetrics = () => {
if (!overallMetricsList || !overallMetricsEmpty) {
return;
}
overallMetricsList.innerHTML = "";
const items = Array.isArray(state.overall_report_metrics)
? state.overall_report_metrics
: [];
items.forEach((item) => {
const li = document.createElement("li");
li.className = "metric-list-item";
li.textContent = `${item.label}: ${item.value}`;
overallMetricsList.appendChild(li);
});
overallMetricsEmpty.hidden = items.length > 0;
};
const renderRecentSimulations = () => {
if (!recentList || !recentEmpty) {
return;
}
recentList.innerHTML = "";
const runs = Array.isArray(state.recent_simulations)
? state.recent_simulations
: [];
runs.forEach((run) => {
const item = document.createElement("li");
item.className = "metric-list-item";
item.textContent = `${run.scenario_name} · ${run.iterations_display} iterations · ${run.mean_display}`;
recentList.appendChild(item);
});
recentEmpty.hidden = runs.length > 0;
};
const renderMaintenanceReminders = () => {
if (!maintenanceList || !maintenanceEmpty) {
return;
}
maintenanceList.innerHTML = "";
const items = Array.isArray(state.upcoming_maintenance)
? state.upcoming_maintenance
: [];
items.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `
<span class="list-title">${item.equipment_name} · ${item.scenario_name}</span>
<span class="list-detail">${item.date_display} · ${item.cost_display} · ${item.description}</span>
`;
maintenanceList.appendChild(li);
});
maintenanceEmpty.hidden = items.length > 0;
};
const buildChartConfig = (dataset, overrides = {}) => ({
type: dataset.type || "bar",
data: {
labels: dataset.labels || [],
datasets: dataset.datasets || [],
},
options: Object.assign(
{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: "top" },
tooltip: { enabled: true },
},
scales: {
x: { stacked: dataset.stacked ?? false },
y: { stacked: dataset.stacked ?? false, beginAtZero: true },
},
},
overrides.options || {}
),
});
const renderCharts = () => {
if (costChartInstance) {
costChartInstance.destroy();
}
if (activityChartInstance) {
activityChartInstance.destroy();
}
const costData = state.scenario_cost_chart || {};
const activityData = state.scenario_activity_chart || {};
if (costChartCanvas && state.cost_chart_has_data) {
costChartInstance = new Chart(
costChartCanvas,
buildChartConfig(costData, {
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) =>
typeof value === "number"
? value.toLocaleString(undefined, {
maximumFractionDigits: 0,
})
: value,
},
},
},
},
})
);
if (costChartEmpty) {
costChartEmpty.hidden = true;
}
costChartCanvas.classList.remove("hidden");
} else if (costChartEmpty && costChartCanvas) {
costChartEmpty.hidden = false;
costChartCanvas.classList.add("hidden");
}
if (activityChartCanvas && state.activity_chart_has_data) {
activityChartInstance = new Chart(
activityChartCanvas,
buildChartConfig(activityData, {
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) =>
typeof value === "number"
? value.toLocaleString(undefined, {
maximumFractionDigits: 0,
})
: value,
},
},
},
},
})
);
if (activityChartEmpty) {
activityChartEmpty.hidden = true;
}
activityChartCanvas.classList.remove("hidden");
} else if (activityChartEmpty && activityChartCanvas) {
activityChartEmpty.hidden = false;
activityChartCanvas.classList.add("hidden");
}
};
const renderView = () => {
renderSummaryMetrics();
renderScenarioTable();
renderOverallMetrics();
renderRecentSimulations();
renderMaintenanceReminders();
renderCharts();
};
const refreshDashboard = async () => {
setStatus("Refreshing dashboard…", "success");
if (refreshButton) {
refreshButton.classList.add("is-loading");
}
try {
const response = await fetch("/ui/dashboard/data", {
headers: { "X-Requested-With": "XMLHttpRequest" },
});
if (!response.ok) {
throw new Error("Unable to refresh dashboard data.");
}
const payload = await response.json();
state = payload || {};
renderView();
setStatus("Dashboard updated.", "success");
} catch (error) {
console.error(error);
setStatus(error.message || "Failed to refresh dashboard.", "error");
} finally {
if (refreshButton) {
refreshButton.classList.remove("is-loading");
}
}
};
renderView();
if (refreshButton) {
refreshButton.addEventListener("click", refreshDashboard);
}
})();

145
static/js/equipment.js Normal file
View File

@@ -0,0 +1,145 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("equipment-data");
let equipmentByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.equipment && typeof parsed.equipment === "object") {
equipmentByScenario = parsed.equipment;
}
}
} catch (error) {
console.error("Unable to parse equipment data", error);
}
}
const filterSelect = document.getElementById("equipment-scenario-filter");
const tableWrapper = document.getElementById("equipment-table-wrapper");
const tableBody = document.getElementById("equipment-table-body");
const emptyState = document.getElementById("equipment-empty");
const form = document.getElementById("equipment-form");
const feedbackEl = document.getElementById("equipment-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const renderEquipmentRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = equipmentByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No equipment recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${record.name || "—"}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review the equipment list.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderEquipmentRows(value);
});
}
const submitEquipment = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
name: formData.get("name"),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/equipment/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add equipment record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(equipmentByScenario[mapKey])) {
equipmentByScenario[mapKey] = [];
}
equipmentByScenario[mapKey].push(result);
form.reset();
showFeedback("Equipment saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderEquipmentRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitEquipment);
}
if (filterSelect && filterSelect.value) {
renderEquipmentRows(filterSelect.value);
}
});

243
static/js/maintenance.js Normal file
View File

@@ -0,0 +1,243 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("maintenance-data");
let equipmentByScenario = {};
let maintenanceByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (parsed.equipment && typeof parsed.equipment === "object") {
equipmentByScenario = parsed.equipment;
}
if (parsed.maintenance && typeof parsed.maintenance === "object") {
maintenanceByScenario = parsed.maintenance;
}
}
} catch (error) {
console.error("Unable to parse maintenance data", error);
}
}
const filterSelect = document.getElementById("maintenance-scenario-filter");
const tableWrapper = document.getElementById("maintenance-table-wrapper");
const tableBody = document.getElementById("maintenance-table-body");
const emptyState = document.getElementById("maintenance-empty");
const form = document.getElementById("maintenance-form");
const feedbackEl = document.getElementById("maintenance-feedback");
const formScenarioSelect = document.getElementById(
"maintenance-form-scenario"
);
const equipmentSelect = document.getElementById("maintenance-form-equipment");
const equipmentEmptyState = document.getElementById(
"maintenance-equipment-empty"
);
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatCost = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const formatDate = (value) => {
if (!value) {
return "—";
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleDateString();
};
const renderMaintenanceRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = maintenanceByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent =
"No maintenance entries recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatDate(record.maintenance_date)}</td>
<td>${record.equipment_name || "—"}</td>
<td>${formatCost(record.cost)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
const populateEquipmentOptions = (scenarioId) => {
if (!equipmentSelect) {
return;
}
equipmentSelect.innerHTML =
'<option value="" disabled selected>Select equipment</option>';
equipmentSelect.disabled = true;
if (equipmentEmptyState) {
equipmentEmptyState.classList.add("hidden");
}
if (!scenarioId) {
return;
}
const list = equipmentByScenario[String(scenarioId)] || [];
if (!list.length) {
if (equipmentEmptyState) {
equipmentEmptyState.textContent =
"Add equipment for this scenario before scheduling maintenance.";
equipmentEmptyState.classList.remove("hidden");
}
return;
}
list.forEach((item) => {
const option = document.createElement("option");
option.value = item.id;
option.textContent = item.name || `Equipment ${item.id}`;
equipmentSelect.appendChild(option);
});
equipmentSelect.disabled = false;
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review upcoming or completed maintenance.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderMaintenanceRows(value);
});
}
if (formScenarioSelect) {
formScenarioSelect.addEventListener("change", (event) => {
const value = event.target.value;
populateEquipmentOptions(value);
});
}
const submitMaintenance = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const equipmentId = formData.get("equipment_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
equipment_id: equipmentId ? Number(equipmentId) : null,
maintenance_date: formData.get("maintenance_date"),
cost: Number(formData.get("cost")),
description: formData.get("description") || null,
};
if (!payload.scenario_id || !payload.equipment_id) {
showFeedback(
"Select a scenario and equipment before submitting.",
"error"
);
return;
}
try {
const response = await fetch("/api/maintenance/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add maintenance entry."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(maintenanceByScenario[mapKey])) {
maintenanceByScenario[mapKey] = [];
}
const equipmentList = equipmentByScenario[mapKey] || [];
const matchedEquipment = equipmentList.find(
(item) => Number(item.id) === Number(result.equipment_id)
);
result.equipment_name = matchedEquipment ? matchedEquipment.name : "";
maintenanceByScenario[mapKey].push(result);
form.reset();
populateEquipmentOptions(null);
showFeedback("Maintenance entry saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderMaintenanceRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitMaintenance);
}
if (filterSelect && filterSelect.value) {
renderMaintenanceRows(filterSelect.value);
}
if (formScenarioSelect && formScenarioSelect.value) {
populateEquipmentOptions(formScenarioSelect.value);
}
});

124
static/js/parameters.js Normal file
View File

@@ -0,0 +1,124 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("parameters-data");
let parametersByScenario = {};
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
parametersByScenario = parsed;
}
} catch (error) {
console.error("Unable to parse parameter data", error);
}
}
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");
});
});

157
static/js/production.js Normal file
View File

@@ -0,0 +1,157 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("production-data");
let data = { scenarios: [], production: {} };
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
data = {
scenarios: Array.isArray(parsed.scenarios) ? parsed.scenarios : [],
production:
parsed.production && typeof parsed.production === "object"
? parsed.production
: {},
};
}
} catch (error) {
console.error("Unable to parse production data", error);
}
}
const productionByScenario = data.production;
const filterSelect = document.getElementById("production-scenario-filter");
const tableWrapper = document.getElementById("production-table-wrapper");
const tableBody = document.getElementById("production-table-body");
const emptyState = document.getElementById("production-empty");
const form = document.getElementById("production-form");
const feedbackEl = document.getElementById("production-feedback");
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const formatAmount = (value) =>
Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const renderProductionRows = (scenarioId) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
const key = String(scenarioId);
const records = productionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent =
"No production output recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
};
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
if (emptyState && tableWrapper && tableBody) {
emptyState.textContent =
"Choose a scenario to review its production output.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
}
return;
}
renderProductionRows(value);
});
}
const submitProduction = async (event) => {
event.preventDefault();
hideFeedback();
if (!form) {
return;
}
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/production/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(
errorDetail.detail || "Unable to add production output record."
);
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(productionByScenario[mapKey])) {
productionByScenario[mapKey] = [];
}
productionByScenario[mapKey].push(result);
form.reset();
showFeedback("Production output saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderProductionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
if (form) {
form.addEventListener("submit", submitProduction);
}
if (filterSelect && filterSelect.value) {
renderProductionRows(filterSelect.value);
}
});

149
static/js/reporting.js Normal file
View File

@@ -0,0 +1,149 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("reporting-data");
let reportingSummaries = [];
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "[]");
if (Array.isArray(parsed)) {
reportingSummaries = parsed;
}
} catch (error) {
console.error("Unable to parse reporting data", error);
}
}
const REPORT_FIELDS = [
{ key: "iterations", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Std. Dev", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{
key: "expected_shortfall_95",
label: "Expected Shortfall (95%)",
decimals: 2,
},
];
const tableWrapper = document.getElementById("reporting-table-wrapper");
const tableBody = document.getElementById("reporting-table-body");
const emptyState = document.getElementById("reporting-empty");
const refreshButton = document.getElementById("report-refresh");
const feedbackEl = document.getElementById("report-feedback");
const formatNumber = (value, decimals = 2) => {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
};
const showFeedback = (message, type = "success") => {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
};
const hideFeedback = () => {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
};
const renderReportingTable = (summaryData) => {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
tableBody.innerHTML = "";
if (!summaryData.length) {
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
summaryData.forEach((entry) => {
const row = document.createElement("tr");
const scenarioCell = document.createElement("td");
scenarioCell.textContent = entry.scenario_name;
row.appendChild(scenarioCell);
REPORT_FIELDS.forEach((field) => {
const cell = document.createElement("td");
const source = field.key === "iterations" ? entry : entry.summary || {};
cell.textContent = formatNumber(source[field.key], field.decimals);
row.appendChild(cell);
});
tableBody.appendChild(row);
});
};
const refreshMetrics = async () => {
hideFeedback();
showFeedback("Refreshing metrics…", "success");
try {
const response = await fetch("/ui/reporting", {
method: "GET",
headers: { "X-Requested-With": "XMLHttpRequest" },
});
if (!response.ok) {
throw new Error("Unable to refresh reporting data.");
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const newTable = doc.querySelector("#reporting-table-wrapper");
const newFeedback = doc.querySelector("#report-feedback");
if (!newTable) {
throw new Error("Unexpected response while refreshing.");
}
const newEmptyState = doc.querySelector("#reporting-empty");
if (emptyState && newEmptyState) {
emptyState.className = newEmptyState.className;
emptyState.textContent = newEmptyState.textContent;
}
if (tableWrapper) {
tableWrapper.className = newTable.className;
tableWrapper.innerHTML = newTable.innerHTML;
}
if (newFeedback && feedbackEl) {
feedbackEl.className = newFeedback.className;
feedbackEl.textContent = newFeedback.textContent;
}
showFeedback("Metrics refreshed.", "success");
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
};
renderReportingTable(reportingSummaries);
if (refreshButton) {
refreshButton.addEventListener("click", refreshMetrics);
}
});

View File

@@ -0,0 +1,69 @@
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("scenario-form");
if (!form) {
return;
}
const nameInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("name")
);
const descriptionInput = /** @type {HTMLInputElement | null} */ (
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();
if (!nameInput || !descriptionInput) {
return;
}
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();
});
});

354
static/js/simulations.js Normal file
View File

@@ -0,0 +1,354 @@
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("simulations-data");
let simulationScenarios = [];
let initialRuns = [];
if (dataElement) {
try {
const parsed = JSON.parse(dataElement.textContent || "{}");
if (parsed && typeof parsed === "object") {
if (Array.isArray(parsed.scenarios)) {
simulationScenarios = parsed.scenarios;
}
if (Array.isArray(parsed.runs)) {
initialRuns = parsed.runs;
}
}
} catch (error) {
console.error("Unable to parse simulations data", error);
}
}
const SUMMARY_FIELDS = [
{ key: "count", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "median", label: "Median Result", decimals: 2 },
{ key: "min", label: "Minimum", decimals: 2 },
{ key: "max", label: "Maximum", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Standard Deviation", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{
key: "expected_shortfall_95",
label: "Expected Shortfall (95%)",
decimals: 2,
},
];
const SAMPLE_RESULT_LIMIT = 20;
const filterSelect = document.getElementById("simulations-scenario-filter");
const overviewWrapper = document.getElementById(
"simulations-overview-wrapper"
);
const overviewBody = document.getElementById("simulations-overview-body");
const overviewEmpty = document.getElementById("simulations-overview-empty");
const emptyState = document.getElementById("simulations-empty");
const summaryWrapper = document.getElementById("simulations-summary-wrapper");
const summaryBody = document.getElementById("simulations-summary-body");
const summaryEmpty = document.getElementById("simulations-summary-empty");
const resultsWrapper = document.getElementById("simulations-results-wrapper");
const resultsBody = document.getElementById("simulations-results-body");
const resultsEmpty = document.getElementById("simulations-results-empty");
const simulationForm = document.getElementById("simulation-run-form");
const simulationFeedback = document.getElementById("simulation-feedback");
const formScenarioSelect = document.getElementById(
"simulation-form-scenario"
);
const simulationRunsMap = Object.create(null);
const getScenarioName = (id) => {
const match = simulationScenarios.find(
(scenario) => String(scenario.id) === String(id)
);
return match ? match.name : `Scenario ${id}`;
};
const formatNumber = (value, decimals = 2) => {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
};
const showFeedback = (element, message, type = "success") => {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
};
const hideFeedback = (element) => {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
};
const initializeRunsMap = () => {
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
simulationRunsMap[key] = {
scenario_id: scenario.id,
scenario_name: scenario.name,
iterations: 0,
summary: null,
sample_results: [],
};
});
initialRuns.forEach((run) => {
const key = String(run.scenario_id);
simulationRunsMap[key] = {
scenario_id: run.scenario_id,
scenario_name: run.scenario_name || getScenarioName(key),
iterations: run.iterations || 0,
summary: run.summary || null,
sample_results: Array.isArray(run.sample_results)
? run.sample_results
: [],
};
});
};
const renderOverviewTable = () => {
if (!overviewBody) {
return;
}
overviewBody.innerHTML = "";
if (!simulationScenarios.length) {
if (overviewWrapper) {
overviewWrapper.classList.add("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.remove("hidden");
}
return;
}
if (overviewWrapper) {
overviewWrapper.classList.remove("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.add("hidden");
}
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
const run = simulationRunsMap[key];
const iterations = run && run.iterations ? run.iterations : 0;
const meanValue =
iterations && run && run.summary ? run.summary.mean : null;
const row = document.createElement("tr");
row.innerHTML = `
<td>${scenario.name}</td>
<td>${iterations || 0}</td>
<td>${iterations ? formatNumber(meanValue) : "—"}</td>
`;
overviewBody.appendChild(row);
});
};
const renderScenarioDetails = (scenarioId) => {
if (!scenarioId) {
if (emptyState) {
emptyState.classList.remove("hidden");
}
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
return;
}
if (emptyState) {
emptyState.classList.add("hidden");
}
const run = simulationRunsMap[String(scenarioId)];
const summary = run ? run.summary : null;
const samples = run ? run.sample_results || [] : [];
if (!summary) {
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.remove("hidden");
}
} else {
if (summaryWrapper) {
summaryWrapper.classList.remove("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (summaryBody) {
summaryBody.innerHTML = "";
SUMMARY_FIELDS.forEach((field) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${field.label}</td>
<td>${formatNumber(summary[field.key], field.decimals)}</td>
`;
summaryBody.appendChild(row);
});
}
}
if (!samples.length) {
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.remove("hidden");
}
} else {
if (resultsWrapper) {
resultsWrapper.classList.remove("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
if (resultsBody) {
resultsBody.innerHTML = "";
samples.slice(0, SAMPLE_RESULT_LIMIT).forEach((item, index) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${index + 1}</td>
<td>${formatNumber(item)}</td>
`;
resultsBody.appendChild(row);
});
}
}
};
const runSimulation = async (event) => {
event.preventDefault();
hideFeedback(simulationFeedback);
if (!simulationForm) {
return;
}
const formData = new FormData(simulationForm);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
iterations: Number(formData.get("iterations")),
seed: formData.get("seed") ? Number(formData.get("seed")) : null,
};
if (!payload.scenario_id) {
showFeedback(
simulationFeedback,
"Select a scenario before running a simulation.",
"error"
);
return;
}
try {
const response = await fetch("/api/simulations/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to run simulation.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
const summary =
result.summary && typeof result.summary === "object"
? result.summary
: null;
const iterations =
summary && typeof summary.count === "number"
? summary.count
: payload.iterations || 0;
simulationRunsMap[mapKey] = {
scenario_id: result.scenario_id,
scenario_name: getScenarioName(mapKey),
iterations,
summary,
sample_results: Array.isArray(result.sample_results)
? result.sample_results
: [],
};
renderOverviewTable();
renderScenarioDetails(mapKey);
if (filterSelect) {
filterSelect.value = mapKey;
}
if (formScenarioSelect) {
formScenarioSelect.value = mapKey;
}
simulationForm.reset();
showFeedback(simulationFeedback, "Simulation completed.", "success");
} catch (error) {
showFeedback(
simulationFeedback,
error.message || "An unexpected error occurred.",
"error"
);
}
};
initializeRunsMap();
renderOverviewTable();
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
renderScenarioDetails(value);
});
}
if (formScenarioSelect) {
formScenarioSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (filterSelect) {
filterSelect.value = value;
}
renderScenarioDetails(value);
});
}
if (simulationForm) {
simulationForm.addEventListener("submit", runSimulation);
}
if (filterSelect && filterSelect.value) {
renderScenarioDetails(filterSelect.value);
}
});

View File

@@ -84,493 +84,11 @@ block content %}
endif endif
%} %}
></canvas> ></canvas>
<p
id="activity-chart-empty"
class="empty-state"
{%
if
activity_chart_has_data
%}
hidden{%
endif
%}
>
Add production or consumption records to display this chart.
</p>
</article> </article>
</section> </section>
<section class="panel dashboard-panel">
<header class="panel-header">
<div>
<h3>Scenario Snapshot</h3>
<p class="chart-subtitle">
Operational and financial highlights per scenario
</p>
</div>
</header>
<div class="table-container">
<table id="scenario-table">
<thead>
<tr>
<th>Scenario</th>
<th>Parameters</th>
<th>Equipment</th>
<th>CAPEX</th>
<th>OPEX</th>
<th>Production</th>
<th>Consumption</th>
<th>Maintenance</th>
<th>Iterations</th>
<th>Simulation Mean</th>
</tr>
</thead>
<tbody>
{% for row in scenario_rows %}
<tr>
<td>{{ row.scenario_name }}</td>
<td>{{ row.parameter_display }}</td>
<td>{{ row.equipment_display }}</td>
<td>{{ row.capex_display }}</td>
<td>{{ row.opex_display }}</td>
<td>{{ row.production_display }}</td>
<td>{{ row.consumption_display }}</td>
<td>{{ row.maintenance_display }}</td>
<td>{{ row.iterations_display }}</td>
<td>{{ row.simulation_mean_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p id="scenario-table-empty" class="empty-state" {% if scenario_rows|length>
0 %} hidden{% endif %}> Create scenarios and populate domain data to see the
snapshot overview.
</p>
</section>
<div class="dashboard-columns">
<section class="panel dashboard-panel">
<header class="panel-header">
<h3>Overall Simulation Metrics</h3>
</header>
<ul id="overall-metrics" class="metric-list">
{% for metric in overall_report_metrics %}
<li>
<span class="metric-label">{{ metric.label }}</span>
<span class="metric-value">{{ metric.value }}</span>
</li>
{% endfor %}
</ul>
<p
id="overall-metrics-empty"
class="empty-state"
{%
if
report_available
%}
hidden{%
endif
%}
>
Run a simulation to surface aggregate reporting metrics.
</p>
</section>
<section class="panel dashboard-panel">
<header class="panel-header">
<h3>Recent Simulation Runs</h3>
</header>
<ul id="recent-simulations" class="striped-list">
{% for run in recent_simulations %}
<li>
<span class="list-title">{{ run.scenario_name }}</span>
<span class="list-detail"
>Iterations: {{ run.iterations_display }} · Mean: {{ run.mean_display
}} · P95: {{ run.p95_display }}</span
>
</li>
{% endfor %}
</ul>
<p
id="recent-simulations-empty"
class="empty-state"
{%
if
recent_simulations|length
>
0 %} hidden{% endif %}> Trigger simulations to populate recent run
statistics.
</p>
</section>
</div>
<section class="panel dashboard-panel">
<header class="panel-header">
<h3>Upcoming Maintenance</h3>
</header>
<ul id="upcoming-maintenance" class="striped-list">
{% for item in upcoming_maintenance %}
<li>
<span class="list-title"
>{{ item.equipment_name }} · {{ item.scenario_name }}</span
>
<span class="list-detail"
>{{ item.date_display }} · {{ item.cost_display }} · {{ item.description
}}</span
>
</li>
{% endfor %}
</ul>
<p
id="upcoming-maintenance-empty"
class="empty-state"
{%
if
upcoming_maintenance|length
>
0 %} hidden{% endif %}> Schedule maintenance activities to track upcoming
events.
</p>
</section>
{% endblock %} {% block scripts %} {{ super() }} {% endblock %} {% block scripts %} {{ super() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script id="dashboard-data" type="application/json"> <script id="dashboard-data" type="application/json">
{{ { {{ {"summary_metrics": summary_metrics, "scenario_rows": scenario_rows, "overall_report_metrics": overall_report_metrics, "recent_simulations": recent_simulations, "upcoming_maintenance": upcoming_maintenance} | tojson }}
"summary_metrics": summary_metrics,
"scenario_rows": scenario_rows,
"overall_report_metrics": overall_report_metrics,
"recent_simulations": recent_simulations,
"upcoming_maintenance": upcoming_maintenance,
"scenario_cost_chart": scenario_cost_chart,
"scenario_activity_chart": scenario_activity_chart,
"cost_chart_has_data": cost_chart_has_data,
"activity_chart_has_data": activity_chart_has_data,
"report_available": report_available
} | tojson }}
</script>
<script>
(() => {
const dataElement = document.getElementById("dashboard-data");
if (!dataElement) {
return;
}
let state = {};
try {
state = JSON.parse(dataElement.textContent || "{}");
} catch (error) {
console.error("Failed to parse dashboard data", error);
return;
}
const statusElement = document.getElementById("dashboard-status");
const summaryContainer = document.getElementById("summary-metrics");
const summaryEmpty = document.getElementById("summary-empty");
const scenarioTableBody = document.querySelector("#scenario-table tbody");
const scenarioEmpty = document.getElementById("scenario-table-empty");
const overallMetricsList = document.getElementById("overall-metrics");
const overallMetricsEmpty = document.getElementById(
"overall-metrics-empty"
);
const recentList = document.getElementById("recent-simulations");
const recentEmpty = document.getElementById("recent-simulations-empty");
const maintenanceList = document.getElementById("upcoming-maintenance");
const maintenanceEmpty = document.getElementById(
"upcoming-maintenance-empty"
);
const refreshButton = document.getElementById("refresh-dashboard");
const costChartCanvas = document.getElementById("cost-chart");
const costChartEmpty = document.getElementById("cost-chart-empty");
const activityChartCanvas = document.getElementById("activity-chart");
const activityChartEmpty = document.getElementById("activity-chart-empty");
let costChartInstance = null;
let activityChartInstance = null;
function setStatus(message, variant = "success") {
if (!statusElement) {
return;
}
if (!message) {
statusElement.hidden = true;
statusElement.textContent = "";
statusElement.classList.remove("success", "error");
return;
}
statusElement.textContent = message;
statusElement.hidden = false;
statusElement.classList.toggle("success", variant === "success");
statusElement.classList.toggle("error", variant !== "success");
}
function renderSummaryMetrics() {
if (!summaryContainer || !summaryEmpty) {
return;
}
summaryContainer.innerHTML = "";
const metrics = Array.isArray(state.summary_metrics)
? state.summary_metrics
: [];
metrics.forEach((metric) => {
const card = document.createElement("article");
card.className = "metric-card";
card.innerHTML = `
<span class="metric-label">${metric.label}</span>
<span class="metric-value">${metric.value}</span>
`;
summaryContainer.appendChild(card);
});
summaryEmpty.hidden = metrics.length > 0;
}
function renderScenarioTable() {
if (!scenarioTableBody || !scenarioEmpty) {
return;
}
scenarioTableBody.innerHTML = "";
const rows = Array.isArray(state.scenario_rows)
? state.scenario_rows
: [];
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${row.scenario_name}</td>
<td>${row.parameter_display}</td>
<td>${row.equipment_display}</td>
<td>${row.capex_display}</td>
<td>${row.opex_display}</td>
<td>${row.production_display}</td>
<td>${row.consumption_display}</td>
<td>${row.maintenance_display}</td>
<td>${row.iterations_display}</td>
<td>${row.simulation_mean_display}</td>
`;
scenarioTableBody.appendChild(tr);
});
scenarioEmpty.hidden = rows.length > 0;
}
function renderOverallMetrics() {
if (!overallMetricsList || !overallMetricsEmpty) {
return;
}
overallMetricsList.innerHTML = "";
const items = Array.isArray(state.overall_report_metrics)
? state.overall_report_metrics
: [];
items.forEach((metric) => {
const li = document.createElement("li");
li.innerHTML = `
<span class="metric-label">${metric.label}</span>
<span class="metric-value">${metric.value}</span>
`;
overallMetricsList.appendChild(li);
});
overallMetricsEmpty.hidden =
Boolean(state.report_available) && items.length > 0;
if (!Boolean(state.report_available)) {
overallMetricsEmpty.hidden = false;
}
}
function renderRecentSimulations() {
if (!recentList || !recentEmpty) {
return;
}
recentList.innerHTML = "";
const runs = Array.isArray(state.recent_simulations)
? state.recent_simulations
: [];
runs.forEach((run) => {
const li = document.createElement("li");
li.innerHTML = `
<span class="list-title">${run.scenario_name}</span>
<span class="list-detail">Iterations: ${run.iterations_display} · Mean: ${run.mean_display} · P95: ${run.p95_display}</span>
`;
recentList.appendChild(li);
});
recentEmpty.hidden = runs.length > 0;
}
function renderMaintenance() {
if (!maintenanceList || !maintenanceEmpty) {
return;
}
maintenanceList.innerHTML = "";
const items = Array.isArray(state.upcoming_maintenance)
? state.upcoming_maintenance
: [];
items.forEach((item) => {
const li = document.createElement("li");
li.innerHTML = `
<span class="list-title">${item.equipment_name} · ${item.scenario_name}</span>
<span class="list-detail">${item.date_display} · ${item.cost_display} · ${item.description}</span>
`;
maintenanceList.appendChild(li);
});
maintenanceEmpty.hidden = items.length > 0;
}
function renderCostChart() {
if (!costChartCanvas || !costChartEmpty) {
return;
}
if (costChartInstance) {
costChartInstance.destroy();
costChartInstance = null;
}
const hasData =
Boolean(state.cost_chart_has_data) &&
Array.isArray(state.scenario_cost_chart?.labels) &&
state.scenario_cost_chart.labels.length > 0;
costChartCanvas.hidden = !hasData;
costChartEmpty.hidden = hasData;
if (!hasData) {
return;
}
costChartInstance = new Chart(costChartCanvas.getContext("2d"), {
type: "bar",
data: {
labels: state.scenario_cost_chart.labels,
datasets: [
{
label: "CAPEX",
data: state.scenario_cost_chart.capex,
backgroundColor: "#1d4ed8",
},
{
label: "OPEX",
data: state.scenario_cost_chart.opex,
backgroundColor: "#38bdf8",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: false,
},
y: {
beginAtZero: true,
},
},
plugins: {
legend: {
position: "bottom",
},
},
},
});
}
function renderActivityChart() {
if (!activityChartCanvas || !activityChartEmpty) {
return;
}
if (activityChartInstance) {
activityChartInstance.destroy();
activityChartInstance = null;
}
const hasData =
Boolean(state.activity_chart_has_data) &&
Array.isArray(state.scenario_activity_chart?.labels) &&
state.scenario_activity_chart.labels.length > 0;
activityChartCanvas.hidden = !hasData;
activityChartEmpty.hidden = hasData;
if (!hasData) {
return;
}
activityChartInstance = new Chart(activityChartCanvas.getContext("2d"), {
type: "line",
data: {
labels: state.scenario_activity_chart.labels,
datasets: [
{
label: "Production",
data: state.scenario_activity_chart.production,
borderColor: "#22c55e",
backgroundColor: "rgba(34, 197, 94, 0.18)",
tension: 0.35,
fill: true,
},
{
label: "Consumption",
data: state.scenario_activity_chart.consumption,
borderColor: "#ef4444",
backgroundColor: "rgba(239, 68, 68, 0.18)",
tension: 0.35,
fill: true,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
}
function renderDashboard() {
renderSummaryMetrics();
renderScenarioTable();
renderOverallMetrics();
renderRecentSimulations();
renderMaintenance();
renderCostChart();
renderActivityChart();
}
function setLoading(isLoading) {
if (!refreshButton) {
return;
}
refreshButton.disabled = isLoading;
refreshButton.classList.toggle("is-loading", isLoading);
refreshButton.textContent = isLoading
? "Refreshing…"
: "Refresh Dashboard";
}
async function refreshDashboard() {
try {
setLoading(true);
setStatus("");
const response = await fetch("/ui/dashboard/data", {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error("Failed to refresh dashboard data.");
}
const payload = await response.json();
state = payload;
renderDashboard();
setStatus("Dashboard updated.", "success");
} catch (error) {
console.error(error);
setStatus(error.message || "Unable to refresh dashboard.", "error");
} finally {
setLoading(false);
}
}
if (refreshButton) {
refreshButton.addEventListener("click", refreshDashboard);
}
renderDashboard();
})();
</script> </script>
<script src="/static/js/dashboard.js"></script>
{% endblock %} {% endblock %}

View File

@@ -44,125 +44,7 @@ endblock %} {% block content %}
</section> </section>
{% endblock %} {% block scripts %} {{ super() }} {% endblock %} {% block scripts %} {{ super() }}
<script id="parameters-data" type="application/json"> <script id="parameters-data" type="application/json">
{{ parameters_by_scenario | tojson | safe }} {{ parameters_by_scenario | tojson }}
</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> </script>
<script src="/static/js/parameters.js"></script>
{% endblock %} {% endblock %}

View File

@@ -48,67 +48,5 @@ endblock %} {% block content %}
</div> </div>
</section> </section>
{% endblock %} {% block scripts %} {{ super() }} {% endblock %} {% block scripts %} {{ super() }}
<script> <script src="/static/js/scenario-form.js"></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 %} {% endblock %}

View File

@@ -71,132 +71,9 @@
{% endif %} {% endif %}
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const scenarios = {{ scenarios | tojson | safe }}; <script id="consumption-data" type="application/json">
const consumptionByScenario = {{ consumption_by_scenario | tojson | safe }}; {{ {"scenarios": scenarios, "consumption": consumption_by_scenario} | tojson }}
const filterSelect = document.getElementById("consumption-scenario-filter");
const tableWrapper = document.getElementById("consumption-table-wrapper");
const tableBody = document.getElementById("consumption-table-body");
const emptyState = document.getElementById("consumption-empty");
const form = document.getElementById("consumption-form");
const feedbackEl = document.getElementById("consumption-feedback");
function showFeedback(message, type = "success") {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
}
function hideFeedback() {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
}
function formatAmount(value) {
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
function renderConsumptionRows(scenarioId) {
const key = String(scenarioId);
const records = consumptionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No consumption records for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
}
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
emptyState.textContent = "Choose a scenario to review its consumption records.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
return;
}
renderConsumptionRows(value);
});
}
async function submitConsumption(event) {
event.preventDefault();
hideFeedback();
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/consumption/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to add consumption record.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(consumptionByScenario[mapKey])) {
consumptionByScenario[mapKey] = [];
}
consumptionByScenario[mapKey].push(result);
form.reset();
showFeedback("Consumption record saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderConsumptionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
}
if (form) {
form.addEventListener("submit", submitConsumption);
}
if (filterSelect && filterSelect.value) {
renderConsumptionRows(filterSelect.value);
}
</script> </script>
<script src="/static/js/consumption.js"></script>
{% endblock %} {% endblock %}

View File

@@ -149,200 +149,9 @@ block content %}
{% endif %} {% endif %}
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const scenarios = {{ scenarios | tojson | safe }}; <script id="costs-payload" type="application/json">
const capexByScenario = {{ capex_by_scenario | tojson | safe }}; {{ {"capex": capex_by_scenario, "opex": opex_by_scenario} | tojson }}
const opexByScenario = {{ opex_by_scenario | tojson | safe }};
const filterSelect = document.getElementById("costs-scenario-filter");
const costsEmptyState = document.getElementById("costs-empty");
const costsDataWrapper = document.getElementById("costs-data");
const capexTableBody = document.getElementById("capex-table-body");
const opexTableBody = document.getElementById("opex-table-body");
const capexEmpty = document.getElementById("capex-empty");
const opexEmpty = document.getElementById("opex-empty");
const capexTotal = document.getElementById("capex-total");
const opexTotal = document.getElementById("opex-total");
const capexForm = document.getElementById("capex-form");
const opexForm = document.getElementById("opex-form");
const capexFeedback = document.getElementById("capex-feedback");
const opexFeedback = document.getElementById("opex-feedback");
const capexFormScenario = document.getElementById("capex-form-scenario");
const opexFormScenario = document.getElementById("opex-form-scenario");
function showFeedback(element, message, type = "success") {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
}
function hideFeedback(element) {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
}
function formatAmount(value) {
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
function sumAmount(records) {
return records.reduce((total, record) => total + Number(record.amount || 0), 0);
}
function renderCostTables(scenarioId) {
const capexRecords = capexByScenario[String(scenarioId)] || [];
const opexRecords = opexByScenario[String(scenarioId)] || [];
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
if (!capexRecords.length) {
capexEmpty.classList.remove("hidden");
} else {
capexEmpty.classList.add("hidden");
capexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
capexTableBody.appendChild(row);
});
}
if (!opexRecords.length) {
opexEmpty.classList.remove("hidden");
} else {
opexEmpty.classList.add("hidden");
opexRecords.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
opexTableBody.appendChild(row);
});
}
capexTotal.textContent = formatAmount(sumAmount(capexRecords));
opexTotal.textContent = formatAmount(sumAmount(opexRecords));
}
function toggleCostView(show) {
if (show) {
costsEmptyState.classList.add("hidden");
costsDataWrapper.classList.remove("hidden");
} else {
costsEmptyState.classList.remove("hidden");
costsDataWrapper.classList.add("hidden");
capexTableBody.innerHTML = "";
opexTableBody.innerHTML = "";
capexTotal.textContent = "—";
opexTotal.textContent = "—";
capexEmpty.classList.add("hidden");
opexEmpty.classList.add("hidden");
}
}
function syncFormSelections(value) {
if (capexFormScenario) {
capexFormScenario.value = value || "";
}
if (opexFormScenario) {
opexFormScenario.value = value || "";
}
}
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
toggleCostView(false);
syncFormSelections("");
return;
}
toggleCostView(true);
renderCostTables(value);
syncFormSelections(value);
});
}
async function submitCostEntry(event, targetUrl, storageMap, feedbackEl) {
event.preventDefault();
hideFeedback(feedbackEl);
const formData = new FormData(event.target);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
if (!payload.scenario_id) {
showFeedback(feedbackEl, "Select a scenario before submitting.", "error");
return;
}
try {
const response = await fetch(targetUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to save cost entry.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(storageMap[mapKey])) {
storageMap[mapKey] = [];
}
storageMap[mapKey].push(result);
event.target.reset();
showFeedback(feedbackEl, "Entry saved successfully.", "success");
if (filterSelect && filterSelect.value === mapKey) {
renderCostTables(mapKey);
}
} catch (error) {
showFeedback(feedbackEl, error.message || "An unexpected error occurred.", "error");
}
}
if (capexForm) {
capexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/capex", capexByScenario, capexFeedback)
);
}
if (opexForm) {
opexForm.addEventListener("submit", (event) =>
submitCostEntry(event, "/api/costs/opex", opexByScenario, opexFeedback)
);
}
if (filterSelect && filterSelect.value) {
toggleCostView(true);
renderCostTables(filterSelect.value);
syncFormSelections(filterSelect.value);
}
</script> </script>
<script src="/static/js/costs.js"></script>
{% endblock %} {% endblock %}

View File

@@ -66,125 +66,9 @@ block content %}
{% endif %} {% endif %}
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const scenarios = {{ scenarios | tojson | safe }}; <script id="equipment-data" type="application/json">
const equipmentByScenario = {{ equipment_by_scenario | tojson | safe }}; {{ {"scenarios": scenarios, "equipment": equipment_by_scenario} | tojson }}
const filterSelect = document.getElementById("equipment-scenario-filter");
const tableWrapper = document.getElementById("equipment-table-wrapper");
const tableBody = document.getElementById("equipment-table-body");
const emptyState = document.getElementById("equipment-empty");
const form = document.getElementById("equipment-form");
const feedbackEl = document.getElementById("equipment-feedback");
function showFeedback(message, type = "success") {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
}
function hideFeedback() {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
}
function renderEquipmentRows(scenarioId) {
const key = String(scenarioId);
const records = equipmentByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No equipment recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${record.name || "—"}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
}
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
emptyState.textContent = "Choose a scenario to review the equipment list.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
return;
}
renderEquipmentRows(value);
});
}
async function submitEquipment(event) {
event.preventDefault();
hideFeedback();
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
name: formData.get("name"),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/equipment/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to add equipment record.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(equipmentByScenario[mapKey])) {
equipmentByScenario[mapKey] = [];
}
equipmentByScenario[mapKey].push(result);
form.reset();
showFeedback("Equipment saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderEquipmentRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
}
if (form) {
form.addEventListener("submit", submitEquipment);
}
if (filterSelect && filterSelect.value) {
renderEquipmentRows(filterSelect.value);
}
</script> </script>
<script src="/static/js/equipment.js"></script>
{% endblock %} {% endblock %}

View File

@@ -100,209 +100,9 @@
{% endif %} {% endif %}
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const scenarios = {{ scenarios | tojson | safe }}; <script id="maintenance-data" type="application/json">
const equipmentByScenario = {{ equipment_by_scenario | tojson | safe }}; {{ {"equipment": equipment_by_scenario, "maintenance": maintenance_by_scenario} | tojson }}
const maintenanceByScenario = {{ maintenance_by_scenario | tojson | safe }};
const filterSelect = document.getElementById("maintenance-scenario-filter");
const tableWrapper = document.getElementById("maintenance-table-wrapper");
const tableBody = document.getElementById("maintenance-table-body");
const emptyState = document.getElementById("maintenance-empty");
const form = document.getElementById("maintenance-form");
const feedbackEl = document.getElementById("maintenance-feedback");
const formScenarioSelect = document.getElementById("maintenance-form-scenario");
const equipmentSelect = document.getElementById("maintenance-form-equipment");
const equipmentEmptyState = document.getElementById("maintenance-equipment-empty");
function showFeedback(message, type = "success") {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
}
function hideFeedback() {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
}
function formatCost(value) {
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
function formatDate(value) {
if (!value) {
return "—";
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleDateString();
}
function renderMaintenanceRows(scenarioId) {
const key = String(scenarioId);
const records = maintenanceByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No maintenance entries recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatDate(record.maintenance_date)}</td>
<td>${record.equipment_name || "—"}</td>
<td>${formatCost(record.cost)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
}
function populateEquipmentOptions(scenarioId) {
if (!equipmentSelect) {
return;
}
equipmentSelect.innerHTML = '<option value="" disabled selected>Select equipment</option>';
equipmentSelect.disabled = true;
if (equipmentEmptyState) {
equipmentEmptyState.classList.add("hidden");
}
if (!scenarioId) {
return;
}
const list = equipmentByScenario[String(scenarioId)] || [];
if (!list.length) {
if (equipmentEmptyState) {
equipmentEmptyState.textContent = "Add equipment for this scenario before scheduling maintenance.";
equipmentEmptyState.classList.remove("hidden");
}
return;
}
list.forEach((item) => {
const option = document.createElement("option");
option.value = item.id;
option.textContent = item.name || `Equipment ${item.id}`;
equipmentSelect.appendChild(option);
});
equipmentSelect.disabled = false;
}
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
emptyState.textContent = "Choose a scenario to review upcoming or completed maintenance.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
return;
}
renderMaintenanceRows(value);
});
}
if (formScenarioSelect) {
formScenarioSelect.addEventListener("change", (event) => {
const value = event.target.value;
populateEquipmentOptions(value);
});
}
async function submitMaintenance(event) {
event.preventDefault();
hideFeedback();
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const equipmentId = formData.get("equipment_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
equipment_id: equipmentId ? Number(equipmentId) : null,
maintenance_date: formData.get("maintenance_date"),
cost: Number(formData.get("cost")),
description: formData.get("description") || null,
};
if (!payload.scenario_id || !payload.equipment_id) {
showFeedback("Select a scenario and equipment before submitting.", "error");
return;
}
try {
const response = await fetch("/api/maintenance/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to add maintenance entry.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(maintenanceByScenario[mapKey])) {
maintenanceByScenario[mapKey] = [];
}
const equipmentList = equipmentByScenario[mapKey] || [];
const matchedEquipment = equipmentList.find(
(item) => Number(item.id) === Number(result.equipment_id)
);
result.equipment_name = matchedEquipment ? matchedEquipment.name : "";
maintenanceByScenario[mapKey].push(result);
form.reset();
populateEquipmentOptions(null);
showFeedback("Maintenance entry saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderMaintenanceRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
}
if (form) {
form.addEventListener("submit", submitMaintenance);
}
if (filterSelect && filterSelect.value) {
renderMaintenanceRows(filterSelect.value);
}
if (formScenarioSelect && formScenarioSelect.value) {
populateEquipmentOptions(formScenarioSelect.value);
}
</script> </script>
<script src="/static/js/maintenance.js"></script>
{% endblock %} {% endblock %}

View File

@@ -73,132 +73,9 @@
{% endif %} {% endif %}
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const scenarios = {{ scenarios | tojson | safe }}; <script id="production-data" type="application/json">
const productionByScenario = {{ production_by_scenario | tojson | safe }}; {{ {"scenarios": scenarios, "production": production_by_scenario} | tojson }}
const filterSelect = document.getElementById("production-scenario-filter");
const tableWrapper = document.getElementById("production-table-wrapper");
const tableBody = document.getElementById("production-table-body");
const emptyState = document.getElementById("production-empty");
const form = document.getElementById("production-form");
const feedbackEl = document.getElementById("production-feedback");
function showFeedback(message, type = "success") {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
}
function hideFeedback() {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
}
function formatAmount(value) {
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
function renderProductionRows(scenarioId) {
const key = String(scenarioId);
const records = productionByScenario[key] || [];
tableBody.innerHTML = "";
if (!records.length) {
emptyState.textContent = "No production output recorded for this scenario yet.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
records.forEach((record) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${formatAmount(record.amount)}</td>
<td>${record.description || "—"}</td>
`;
tableBody.appendChild(row);
});
}
if (filterSelect) {
filterSelect.addEventListener("change", (event) => {
const value = event.target.value;
if (!value) {
emptyState.textContent = "Choose a scenario to review its production output.";
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
tableBody.innerHTML = "";
return;
}
renderProductionRows(value);
});
}
async function submitProduction(event) {
event.preventDefault();
hideFeedback();
const formData = new FormData(form);
const scenarioId = formData.get("scenario_id");
const payload = {
scenario_id: scenarioId ? Number(scenarioId) : null,
amount: Number(formData.get("amount")),
description: formData.get("description") || null,
};
try {
const response = await fetch("/api/production/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to add production output record.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
if (!Array.isArray(productionByScenario[mapKey])) {
productionByScenario[mapKey] = [];
}
productionByScenario[mapKey].push(result);
form.reset();
showFeedback("Production output saved.", "success");
if (filterSelect && filterSelect.value === String(result.scenario_id)) {
renderProductionRows(filterSelect.value);
}
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
}
if (form) {
form.addEventListener("submit", submitProduction);
}
if (filterSelect && filterSelect.value) {
renderProductionRows(filterSelect.value);
}
</script> </script>
<script src="/static/js/production.js"></script>
{% endblock %} {% endblock %}

View File

@@ -33,138 +33,9 @@ block content %}
</div> </div>
</section> </section>
<script> {% endblock %} {% block scripts %} {{ super() }}
const reportingSummaries = {{ report_summaries | tojson | safe }}; <script id="reporting-data" type="application/json">
const REPORT_FIELDS = [ {{ report_summaries | tojson }}
{ key: "iterations", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Std. Dev", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{ key: "expected_shortfall_95", label: "Expected Shortfall (95%)", decimals: 2 },
];
const tableWrapper = document.getElementById("reporting-table-wrapper");
const tableBody = document.getElementById("reporting-table-body");
const emptyState = document.getElementById("reporting-empty");
const refreshButton = document.getElementById("report-refresh");
const feedbackEl = document.getElementById("report-feedback");
function formatNumber(value, decimals = 2) {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
function showFeedback(message, type = "success") {
if (!feedbackEl) {
return;
}
feedbackEl.textContent = message;
feedbackEl.classList.remove("hidden", "success", "error");
feedbackEl.classList.add(type);
}
function hideFeedback() {
if (!feedbackEl) {
return;
}
feedbackEl.classList.add("hidden");
feedbackEl.textContent = "";
}
function renderReportingTable(summaryData) {
if (!tableBody || !tableWrapper || !emptyState) {
return;
}
tableBody.innerHTML = "";
if (!summaryData.length) {
emptyState.classList.remove("hidden");
tableWrapper.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tableWrapper.classList.remove("hidden");
summaryData.forEach((entry) => {
const row = document.createElement("tr");
const scenarioCell = document.createElement("td");
scenarioCell.textContent = entry.scenario_name;
row.appendChild(scenarioCell);
REPORT_FIELDS.forEach((field) => {
const cell = document.createElement("td");
const source = field.key === "iterations" ? entry : entry.summary || {};
cell.textContent = formatNumber(source[field.key], field.decimals);
row.appendChild(cell);
});
tableBody.appendChild(row);
});
}
async function refreshMetrics() {
hideFeedback();
showFeedback("Refreshing metrics…", "success");
try {
const response = await fetch("/ui/reporting", {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
if (!response.ok) {
throw new Error("Unable to refresh reporting data.");
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const newTable = doc.querySelector("#reporting-table-wrapper");
const newFeedback = doc.querySelector("#report-feedback");
if (!newTable) {
throw new Error("Unexpected response while refreshing.");
}
const newEmptyState = doc.querySelector("#reporting-empty");
if (emptyState && newEmptyState) {
emptyState.className = newEmptyState.className;
emptyState.textContent = newEmptyState.textContent;
}
if (tableWrapper) {
tableWrapper.className = newTable.className;
tableWrapper.innerHTML = newTable.innerHTML;
}
if (newFeedback && feedbackEl) {
feedbackEl.className = newFeedback.className;
feedbackEl.textContent = newFeedback.textContent;
}
showFeedback("Metrics refreshed.", "success");
} catch (error) {
showFeedback(error.message || "An unexpected error occurred.", "error");
}
}
renderReportingTable(reportingSummaries);
if (refreshButton) {
refreshButton.addEventListener("click", refreshMetrics);
}
</script> </script>
<script src="/static/js/reporting.js"></script>
{% endblock %} {% endblock %}

View File

@@ -31,417 +31,11 @@
<th scope="col">Mean Result</th> <th scope="col">Mean Result</th>
</tr> </tr>
</thead> </thead>
<tbody id="simulations-overview-body"></tbody> </section>
</table>
</div>
<p id="simulations-overview-empty" class="empty-state hidden">
Create a scenario to review simulation history.
</p>
<div id="simulations-empty" class="empty-state"> {% endblock %} {% block scripts %} {{ super() }}
Select a scenario to review simulation outputs. <script id="simulations-data" type="application/json">
</div> {{ {"scenarios": simulation_scenarios, "runs": simulation_runs} | tojson }}
</script>
<div id="simulations-summary-wrapper" class="table-container hidden"> <script src="/static/js/simulations.js"></script>
<h3>Summary Metrics</h3> {% endblock %}
<table aria-label="Simulation summary metrics">
<thead>
<tr>
<th scope="col">Metric</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody id="simulations-summary-body"></tbody>
</table>
</div>
<p id="simulations-summary-empty" class="empty-state hidden">
No simulations have been run for this scenario yet.
</p>
<div id="simulations-results-wrapper" class="table-container hidden">
<h3>Sample Results</h3>
<table aria-label="Simulation sample results">
<thead>
<tr>
<th scope="col">Iteration</th>
<th scope="col">Result</th>
</tr>
</thead>
<tbody id="simulations-results-body"></tbody>
</table>
</div>
<p id="simulations-results-empty" class="empty-state hidden">
No sample results available for this scenario.
</p>
</section>
<section class="panel">
<h2>Run Simulation</h2>
{% if simulation_scenarios %}
<form id="simulation-run-form" class="form-grid">
<label for="simulation-form-scenario">
Scenario
<select id="simulation-form-scenario" name="scenario_id" required>
<option value="" disabled selected>Select a scenario</option>
{% for scenario in simulation_scenarios %}
<option value="{{ scenario.id }}">{{ scenario.name }}</option>
{% endfor %}
</select>
</label>
<label for="simulation-form-iterations">
Iterations
<input
id="simulation-form-iterations"
type="number"
name="iterations"
min="100"
step="100"
value="1000"
required
/>
</label>
<label for="simulation-form-seed">
Seed (optional)
<input id="simulation-form-seed" type="number" name="seed" />
</label>
<button type="submit" class="btn primary">Run Simulation</button>
</form>
<p id="simulation-feedback" class="feedback hidden" role="status"></p>
{% else %}
<p class="empty-state">Create at least one scenario to run simulations.</p>
{% endif %}
</section>
<script>
const simulationScenarios = {{ simulation_scenarios | tojson | safe }};
const initialRuns = {{ simulation_runs | tojson | safe }};
const SUMMARY_FIELDS = [
{ key: "count", label: "Iterations", decimals: 0 },
{ key: "mean", label: "Mean Result", decimals: 2 },
{ key: "median", label: "Median Result", decimals: 2 },
{ key: "min", label: "Minimum", decimals: 2 },
{ key: "max", label: "Maximum", decimals: 2 },
{ key: "variance", label: "Variance", decimals: 2 },
{ key: "std_dev", label: "Standard Deviation", decimals: 2 },
{ key: "percentile_5", label: "Percentile 5", decimals: 2 },
{ key: "percentile_95", label: "Percentile 95", decimals: 2 },
{ key: "value_at_risk_95", label: "Value at Risk (95%)", decimals: 2 },
{ key: "expected_shortfall_95", label: "Expected Shortfall (95%)", decimals: 2 },
];
const SAMPLE_RESULT_LIMIT = 20;
const filterSelect = document.getElementById("simulations-scenario-filter");
const overviewWrapper = document.getElementById("simulations-overview-wrapper");
const overviewBody = document.getElementById("simulations-overview-body");
const overviewEmpty = document.getElementById("simulations-overview-empty");
const emptyState = document.getElementById("simulations-empty");
const summaryWrapper = document.getElementById("simulations-summary-wrapper");
const summaryBody = document.getElementById("simulations-summary-body");
const summaryEmpty = document.getElementById("simulations-summary-empty");
const resultsWrapper = document.getElementById("simulations-results-wrapper");
const resultsBody = document.getElementById("simulations-results-body");
const resultsEmpty = document.getElementById("simulations-results-empty");
const simulationForm = document.getElementById("simulation-run-form");
const simulationFeedback = document.getElementById("simulation-feedback");
const formScenarioSelect = document.getElementById("simulation-form-scenario");
const simulationRunsMap = Object.create(null);
function getScenarioName(id) {
const match = simulationScenarios.find(
(scenario) => String(scenario.id) === String(id)
);
return match ? match.name : `Scenario ${id}`;
}
function formatNumber(value, decimals = 2) {
if (value === null || value === undefined || Number.isNaN(Number(value))) {
return "—";
}
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
function showFeedback(element, message, type = "success") {
if (!element) {
return;
}
element.textContent = message;
element.classList.remove("hidden", "success", "error");
element.classList.add(type);
}
function hideFeedback(element) {
if (!element) {
return;
}
element.classList.add("hidden");
element.textContent = "";
}
function initializeRunsMap() {
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
simulationRunsMap[key] = {
scenario_id: scenario.id,
scenario_name: scenario.name,
iterations: 0,
summary: null,
sample_results: [],
};
});
initialRuns.forEach((run) => {
const key = String(run.scenario_id);
simulationRunsMap[key] = {
scenario_id: run.scenario_id,
scenario_name: run.scenario_name || getScenarioName(key),
iterations: run.iterations || 0,
summary: run.summary || null,
sample_results: Array.isArray(run.sample_results)
? run.sample_results
: [],
};
});
}
function renderOverviewTable() {
if (!overviewBody) {
return;
}
overviewBody.innerHTML = "";
if (!simulationScenarios.length) {
if (overviewWrapper) {
overviewWrapper.classList.add("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.remove("hidden");
}
return;
}
if (overviewWrapper) {
overviewWrapper.classList.remove("hidden");
}
if (overviewEmpty) {
overviewEmpty.classList.add("hidden");
}
simulationScenarios.forEach((scenario) => {
const key = String(scenario.id);
const run = simulationRunsMap[key];
const iterations = run && run.iterations ? run.iterations : 0;
const meanValue = iterations && run && run.summary ? run.summary.mean : null;
const row = document.createElement("tr");
row.innerHTML = `
<td>${scenario.name}</td>
<td>${iterations || 0}</td>
<td>${iterations ? formatNumber(meanValue) : "—"}</td>
`;
overviewBody.appendChild(row);
});
}
function renderScenarioDetails(scenarioId) {
if (!scenarioId) {
if (emptyState) {
emptyState.classList.remove("hidden");
}
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
return;
}
if (emptyState) {
emptyState.classList.add("hidden");
}
const run = simulationRunsMap[scenarioId];
const iterations = run && run.iterations ? run.iterations : 0;
const summary = run ? run.summary : null;
if (!iterations || !summary) {
if (summaryWrapper) {
summaryWrapper.classList.add("hidden");
}
if (summaryEmpty) {
summaryEmpty.classList.remove("hidden");
}
} else {
if (summaryEmpty) {
summaryEmpty.classList.add("hidden");
}
if (summaryWrapper) {
summaryWrapper.classList.remove("hidden");
}
if (summaryBody) {
summaryBody.innerHTML = "";
SUMMARY_FIELDS.forEach((field) => {
const value = summary[field.key];
const row = document.createElement("tr");
row.innerHTML = `
<td>${field.label}</td>
<td>${formatNumber(value, field.decimals)}</td>
`;
summaryBody.appendChild(row);
});
}
}
const sample = run && Array.isArray(run.sample_results) ? run.sample_results : [];
if (!sample.length) {
if (resultsWrapper) {
resultsWrapper.classList.add("hidden");
}
if (resultsEmpty) {
resultsEmpty.classList.remove("hidden");
}
} else {
if (resultsEmpty) {
resultsEmpty.classList.add("hidden");
}
if (resultsWrapper) {
resultsWrapper.classList.remove("hidden");
}
if (resultsBody) {
resultsBody.innerHTML = "";
sample.forEach((entry) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${entry.iteration}</td>
<td>${formatNumber(entry.result)}</td>
`;
resultsBody.appendChild(row);
});
}
}
}
function syncFormScenario(value) {
if (formScenarioSelect) {
formScenarioSelect.value = value || "";
}
}
function handleScenarioChange(event) {
const value = event.target.value;
renderScenarioDetails(value);
syncFormScenario(value);
renderOverviewTable();
}
async function submitSimulation(event) {
event.preventDefault();
hideFeedback(simulationFeedback);
const formData = new FormData(simulationForm);
const scenarioId = formData.get("scenario_id");
const iterationsValue = Number(formData.get("iterations"));
const seedValue = formData.get("seed");
if (!scenarioId) {
showFeedback(simulationFeedback, "Select a scenario before running a simulation.", "error");
return;
}
if (!iterationsValue || iterationsValue <= 0) {
showFeedback(simulationFeedback, "Provide a positive number of iterations.", "error");
return;
}
const payload = {
scenario_id: Number(scenarioId),
iterations: iterationsValue,
};
if (seedValue) {
payload.seed = Number(seedValue);
}
try {
const response = await fetch("/api/simulations/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorDetail = await response.json().catch(() => ({}));
throw new Error(errorDetail.detail || "Unable to start simulation.");
}
const result = await response.json();
const mapKey = String(result.scenario_id);
const scenarioName = getScenarioName(mapKey);
const sampleResults = Array.isArray(result.results)
? result.results.slice(0, SAMPLE_RESULT_LIMIT).map((entry) => ({
iteration: entry.iteration,
result: entry.result,
}))
: [];
simulationRunsMap[mapKey] = {
scenario_id: result.scenario_id,
scenario_name: scenarioName,
iterations:
(result.summary && Number(result.summary.count)) || result.iterations || sampleResults.length,
summary: result.summary || null,
sample_results: sampleResults,
};
simulationForm.reset();
showFeedback(simulationFeedback, "Simulation completed successfully.", "success");
if (formScenarioSelect) {
formScenarioSelect.value = mapKey;
}
if (filterSelect) {
filterSelect.value = mapKey;
}
renderOverviewTable();
renderScenarioDetails(mapKey);
} catch (error) {
showFeedback(
simulationFeedback,
error.message || "An unexpected error occurred.",
"error"
);
}
}
initializeRunsMap();
renderOverviewTable();
if (filterSelect) {
filterSelect.addEventListener("change", handleScenarioChange);
}
if (simulationForm) {
simulationForm.addEventListener("submit", submitSimulation);
}
if (filterSelect && filterSelect.value) {
renderScenarioDetails(filterSelect.value);
syncFormScenario(filterSelect.value);
}
</script>
{% endblock %}