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:
@@ -31,417 +31,11 @@
|
||||
<th scope="col">Mean Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="simulations-overview-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="simulations-overview-empty" class="empty-state hidden">
|
||||
Create a scenario to review simulation history.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div id="simulations-empty" class="empty-state">
|
||||
Select a scenario to review simulation outputs.
|
||||
</div>
|
||||
|
||||
<div id="simulations-summary-wrapper" class="table-container hidden">
|
||||
<h3>Summary Metrics</h3>
|
||||
<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 %}
|
||||
{% endblock %} {% block scripts %} {{ super() }}
|
||||
<script id="simulations-data" type="application/json">
|
||||
{{ {"scenarios": simulation_scenarios, "runs": simulation_runs} | tojson }}
|
||||
</script>
|
||||
<script src="/static/js/simulations.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user