feat: Add main CSS styles for the application

feat: Refactor Dashboard template to extend base layout and improve structure

feat: Enhance Parameter Input template with improved layout and feedback mechanisms

feat: Update Scenario Form template to utilize base layout and improve user experience

feat: Create base layout template for consistent styling across pages

feat: Add Consumption, Costs, Equipment, Maintenance, Production, Reporting, and Simulations templates with placeholders for future functionality

feat: Implement base header and footer partials for consistent navigation and footer across the application
This commit is contained in:
2025-10-20 22:58:59 +02:00
parent c6233e1a56
commit 5ecd2b8d19
19 changed files with 1051 additions and 368 deletions

View File

@@ -18,6 +18,7 @@ A range of features are implemented to support these functionalities.
- **Equipment Management**: Register scenario-specific equipment inventories.
- **Maintenance Logging**: Log maintenance events against equipment with dates and costs.
- **Reporting Dashboard**: Surface aggregated statistics for simulation outputs with an interactive Chart.js dashboard.
- **Unified UI Shell**: Server-rendered templates extend a shared base layout with navigation across scenarios, parameters, costs, consumption, production, equipment, maintenance, simulations, and reporting views.
- **Monte Carlo Simulation (in progress)**: Services and routes are scaffolded for future stochastic analysis.
## Architecture
@@ -78,6 +79,11 @@ uvicorn main:app --reload
- `POST /api/equipment/` create equipment records
- `POST /api/maintenance/` log maintenance events
- `POST /api/reporting/summary` aggregate simulation results, returning count, mean/median, min/max, standard deviation, variance, percentile bands (5/10/90/95), value-at-risk (95%) and expected shortfall (95%)
- **UI entries** (rendered via FastAPI templates):
- `GET /ui/dashboard` reporting dashboard
- `GET /ui/scenarios` scenario creation form
- `GET /ui/parameters` parameter input form
- `GET /ui/costs`, `/ui/consumption`, `/ui/production`, `/ui/equipment`, `/ui/maintenance`, `/ui/simulations`, `/ui/reporting` placeholder views aligned with future integrations
### Dashboard Preview

View File

@@ -13,7 +13,7 @@ The backend leverages SQLAlchemy for ORM mapping to a PostgreSQL database.
- **FastAPI backend** (`main.py`, `routes/`): hosts REST endpoints for scenarios, parameters, costs, consumption, production, equipment, maintenance, simulations, and reporting. Each router encapsulates request/response schemas and DB access patterns, leveraging a shared dependency module (`routes/dependencies.get_db`) for SQLAlchemy session management.
- **Service layer** (`services/`): houses business logic. `services/reporting.py` produces statistical summaries, while `services/simulation.py` provides the Monte Carlo integration point.
- **Persistence** (`models/`, `config/database.py`): SQLAlchemy models map to PostgreSQL tables in schema `bricsium_platform`. Relationships connect scenarios to derived domain entities.
- **Presentation** (`templates/`, `components/`): server-rendered views support data entry (scenario and parameter forms) and the dashboard visualization powered by Chart.js.
- **Presentation** (`templates/`, `components/`): server-rendered views extend a shared `base.html` layout, pull global styles from `static/css/main.css`, and surface data entry (scenario and parameter forms) alongside the Chart.js-powered dashboard.
- **Middleware** (`middleware/validation.py`): applies JSON validation before requests reach routers.
- **Testing** (`tests/unit/`): pytest suite covering route and service behavior.
@@ -90,3 +90,50 @@ For extended diagrams and setup instructions reference:
- [docs/mvp.md](mvp.md) — roadmap and milestone scope.
- [docs/implementation_plan.md](implementation_plan.md) — feature breakdown aligned with the TODO tracker.
- [docs/architecture_overview.md](architecture_overview.md) — supplementary module map and request flow diagram.
### UI Frontend-Backend Integration Requirements — 2025-10-20
#### Scenarios (`templates/ScenarioForm.html`)
- **Data**: `GET /api/scenarios/` to list existing scenarios for navigation and to hydrate dropdowns in downstream forms; optional aggregation of scenario counts for dashboard badges.
- **Actions**: `POST /api/scenarios/` to create new scenarios; future delete/update flows would reuse the same router once endpoints exist.
#### Parameters (`templates/ParameterInput.html`)
- **Data**: Scenario catalogue from `GET /api/scenarios/`; parameter inventory via `GET /api/parameters/` with client-side filtering by `scenario_id`; optional distribution catalogue from `models/distribution` when exposed.
- **Actions**: `POST /api/parameters/` to add parameters; extend UI to support editing or deleting parameters as routes arrive.
#### Costs (`templates/costs.html`)
- **Data**: CAPEX list `GET /api/costs/capex`; OPEX list `GET /api/costs/opex`; computed totals grouped by scenario for summary panels.
- **Actions**: `POST /api/costs/capex` and `POST /api/costs/opex` for new entries; planned future edits/deletes once routers expand.
#### Consumption (`templates/consumption.html`)
- **Data**: Consumption entries by scenario via `GET /api/consumption/`; scenario metadata for filtering and empty-state messaging.
- **Actions**: `POST /api/consumption/` to log consumption items; include optimistic UI refresh after persistence.
#### Production (`templates/production.html`)
- **Data**: Production records from `GET /api/production/`; scenario list for filter chips; optional aggregates (totals, averages) derived client-side.
- **Actions**: `POST /api/production/` to capture production outputs; notify users when data drives downstream reporting.
#### Equipment (`templates/equipment.html`)
- **Data**: Equipment roster from `GET /api/equipment/`; scenario context to scope inventory; maintenance counts per asset once joins are introduced.
- **Actions**: `POST /api/equipment/` to add equipment; integrate delete/edit when endpoints arrive.
#### Maintenance (`templates/maintenance.html`)
- **Data**: Maintenance schedule `GET /api/maintenance/` with pagination support (`skip`, `limit`); equipment list (`GET /api/equipment/`) to map IDs to names; scenario catalogue for filtering.
- **Actions**: CRUD operations through `POST /api/maintenance/`, `PUT /api/maintenance/{id}`, `DELETE /api/maintenance/{id}`; view detail via `GET /api/maintenance/{id}` for modal display.
#### Simulations (`templates/simulations.html`)
- **Data**: Scenario list `GET /api/scenarios/`; parameter sets `GET /api/parameters/` filtered client-side; persisted simulation results (future) via dedicated endpoint or by caching `/api/simulations/run` responses.
- **Actions**: `POST /api/simulations/run` to execute simulations; surface run configuration (iterations, seed) and feed response summary into reporting page.
#### Reporting (`templates/reporting.html` and `templates/Dashboard.html`)
- **Data**: Simulation outputs either from recent `/api/simulations/run` calls or a planned `GET` endpoint against `simulation_result`; summary metrics via `POST /api/reporting/summary`; scenario metadata for header context.
- **Actions**: Trigger summary refreshes by posting batched results; allow export/download actions once implemented; integrate future comparison requests across scenarios.

17
main.py
View File

@@ -1,8 +1,10 @@
from routes.distributions import router as distributions_router
from routes.ui import router as ui_router
from routes.parameters import router as parameters_router
from fastapi import FastAPI
from fastapi.middleware import Middleware
from typing import Awaitable, Callable
from fastapi import FastAPI, Request, Response
from fastapi.staticfiles import StaticFiles
from middleware.validation import validate_json
from config.database import Base, engine
from routes.scenarios import router as scenarios_router
@@ -18,8 +20,15 @@ from routes.maintenance import router as maintenance_router
Base.metadata.create_all(bind=engine)
app = FastAPI()
# Register validation middleware
app.middleware("http")(validate_json)
@app.middleware("http")
async def json_validation(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
return await validate_json(request, call_next)
app.mount("/static", StaticFiles(directory="static"), name="static")
# Include API routers
app.include_router(scenarios_router)

View File

@@ -1,7 +1,10 @@
from fastapi import Request, HTTPException
from typing import Awaitable, Callable
from fastapi import HTTPException, Request, Response
async def validate_json(request: Request, call_next):
MiddlewareCallNext = Callable[[Request], Awaitable[Response]]
async def validate_json(request: Request, call_next: MiddlewareCallNext) -> Response:
# Only validate JSON for requests with a body
if request.method in ("POST", "PUT", "PATCH"):
try:

View File

@@ -1,6 +1,15 @@
from fastapi import APIRouter, Request
from collections import defaultdict
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from models.parameters import Parameter
from models.scenario import Scenario
from routes.dependencies import get_db
router = APIRouter()
@@ -8,13 +17,138 @@ router = APIRouter()
templates = Jinja2Templates(directory="templates")
def _context(request: Request, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"request": request,
"current_year": datetime.now(timezone.utc).year,
}
if extra:
payload.update(extra)
return payload
def _render(
request: Request,
template_name: str,
extra: Optional[Dict[str, Any]] = None,
):
return templates.TemplateResponse(template_name, _context(request, extra))
def _load_scenarios(db: Session) -> Dict[str, Any]:
scenarios: list[Dict[str, Any]] = [
{
"id": item.id,
"name": item.name,
"description": item.description,
}
for item in db.query(Scenario).order_by(Scenario.name).all()
]
return {"scenarios": scenarios}
def _load_parameters(db: Session) -> Dict[str, Any]:
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
for param in db.query(Parameter).order_by(Parameter.scenario_id, Parameter.id):
grouped[param.scenario_id].append(
{
"id": param.id,
"name": param.name,
"value": param.value,
"distribution_type": param.distribution_type,
"distribution_parameters": param.distribution_parameters,
}
)
return {"parameters_by_scenario": dict(grouped)}
def _load_costs(_: Session) -> Dict[str, Any]:
return {"capex_entries": [], "opex_entries": []}
def _load_consumption(_: Session) -> Dict[str, Any]:
return {"consumption_records": []}
def _load_production(_: Session) -> Dict[str, Any]:
return {"production_records": []}
def _load_equipment(_: Session) -> Dict[str, Any]:
return {"equipment_items": []}
def _load_maintenance(_: Session) -> Dict[str, Any]:
return {"maintenance_records": []}
def _load_simulations(_: Session) -> Dict[str, Any]:
return {"simulation_runs": []}
def _load_reporting(_: Session) -> Dict[str, Any]:
return {"report_summaries": []}
@router.get("/ui/scenarios", response_class=HTMLResponse)
async def scenario_form(request: Request):
async def scenario_form(request: Request, db: Session = Depends(get_db)):
"""Render the scenario creation form."""
return templates.TemplateResponse("ScenarioForm.html", {"request": request})
context = _load_scenarios(db)
return _render(request, "ScenarioForm.html", context)
@router.get("/ui/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
"""Render the reporting dashboard."""
return _render(request, "Dashboard.html")
@router.get("/ui/parameters", response_class=HTMLResponse)
async def parameter_form(request: Request):
async def parameter_form(request: Request, db: Session = Depends(get_db)):
"""Render the parameter input form."""
return templates.TemplateResponse("ParameterInput.html", {"request": request})
context: Dict[str, Any] = {}
context.update(_load_scenarios(db))
context.update(_load_parameters(db))
return _render(request, "ParameterInput.html", context)
@router.get("/ui/costs", response_class=HTMLResponse)
async def costs_view(request: Request, db: Session = Depends(get_db)):
"""Render the costs placeholder view."""
return _render(request, "costs.html", _load_costs(db))
@router.get("/ui/consumption", response_class=HTMLResponse)
async def consumption_view(request: Request, db: Session = Depends(get_db)):
"""Render the consumption placeholder view."""
return _render(request, "consumption.html", _load_consumption(db))
@router.get("/ui/production", response_class=HTMLResponse)
async def production_view(request: Request, db: Session = Depends(get_db)):
"""Render the production placeholder view."""
return _render(request, "production.html", _load_production(db))
@router.get("/ui/equipment", response_class=HTMLResponse)
async def equipment_view(request: Request, db: Session = Depends(get_db)):
"""Render the equipment placeholder view."""
return _render(request, "equipment.html", _load_equipment(db))
@router.get("/ui/maintenance", response_class=HTMLResponse)
async def maintenance_view(request: Request, db: Session = Depends(get_db)):
"""Render the maintenance placeholder view."""
return _render(request, "maintenance.html", _load_maintenance(db))
@router.get("/ui/simulations", response_class=HTMLResponse)
async def simulations_view(request: Request, db: Session = Depends(get_db)):
"""Render the simulations placeholder view."""
return _render(request, "simulations.html", _load_simulations(db))
@router.get("/ui/reporting", response_class=HTMLResponse)
async def reporting_view(request: Request, db: Session = Depends(get_db)):
"""Render the reporting placeholder view."""
return _render(request, "reporting.html", _load_reporting(db))

209
static/css/main.css Normal file
View File

@@ -0,0 +1,209 @@
body {
margin: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f5f7;
color: #1f2933;
}
.container {
width: min(960px, 92%);
margin: 0 auto;
padding: 2rem 0;
}
.site-header {
background-color: #0b3d91;
color: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
.site-title {
font-size: 1.5rem;
margin: 0;
}
.site-nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.site-nav a {
color: #ffffff;
text-decoration: none;
font-weight: 500;
}
.site-nav a:hover,
.site-nav a:focus {
text-decoration: underline;
}
main.container {
padding-top: 2rem;
padding-bottom: 2rem;
}
.panel {
background-color: #ffffff;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
margin-bottom: 2rem;
}
.form-grid {
display: grid;
gap: 1rem;
max-width: 480px;
}
.form-grid label {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-weight: 600;
color: #111827;
}
.form-grid input,
.form-grid textarea,
.form-grid select {
padding: 0.6rem 0.75rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 1rem;
}
.form-grid input:focus,
.form-grid textarea:focus,
.form-grid select:focus {
outline: 2px solid #2563eb;
outline-offset: 1px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.65rem 1.25rem;
border-radius: 999px;
border: none;
cursor: pointer;
font-weight: 600;
background-color: #e2e8f0;
color: #0f172a;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.btn:hover,
.btn:focus {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.1);
}
.btn.primary {
background-color: #2563eb;
color: #ffffff;
}
.btn.primary:hover,
.btn.primary:focus {
background-color: #1d4ed8;
}
.result-output {
background-color: #0f172a;
color: #f8fafc;
padding: 1rem;
border-radius: 8px;
font-family: "Fira Code", "Consolas", "Courier New", monospace;
overflow-x: auto;
margin-top: 1.5rem;
}
.monospace-input {
width: 100%;
font-family: "Fira Code", "Consolas", "Courier New", monospace;
min-height: 120px;
}
.button-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.table-container {
margin-top: 1.5rem;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
border-radius: 12px;
overflow: hidden;
background-color: #f8fafc;
}
thead {
background-color: #0b3d91;
color: #ffffff;
}
th,
td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
tbody tr:nth-child(even) {
background-color: #eef2ff;
}
.empty-state {
margin-top: 1.5rem;
color: #64748b;
font-style: italic;
}
.hidden {
display: none;
}
.feedback {
margin-top: 1rem;
font-weight: 600;
}
.feedback.success {
color: #047857;
}
.feedback.error {
color: #b91c1c;
}
.site-footer {
background-color: #0b3d91;
color: #ffffff;
margin-top: 3rem;
}
.footer-inner {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 0;
font-size: 0.9rem;
}

View File

@@ -1,19 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>CalMiner Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 2rem;
background-color: #f4f5f7;
color: #1f2933;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
block head_extra %} {{ super() }}
<style>
.summary-card {
background: #ffffff;
border-radius: 8px;
@@ -50,37 +37,35 @@
color: #b91d47;
margin-top: 1rem;
}
</style>
</head>
<body>
<h1>Simulation Results Dashboard</h1>
<div class="summary-card">
<h2>Summary Statistics</h2>
</style>
{% endblock %} {% block content %}
<h2>Simulation Results Dashboard</h2>
<div class="summary-card">
<h3>Summary Statistics</h3>
<div id="summary-grid" class="summary-grid"></div>
<p id="error-message" hidden></p>
</div>
<div class="summary-card">
<h2>Sample Results Input</h2>
</div>
<div class="summary-card">
<h3>Sample Results Input</h3>
<p>
Provide simulation outputs as JSON (array of objects containing the
<code>result</code> field) and refresh the dashboard to preview metrics.
</p>
<textarea
id="results-input"
rows="6"
style="width: 100%; font-family: monospace"
></textarea>
<div style="margin-top: 1rem; display: flex; gap: 0.5rem">
<button id="load-sample" type="button">Load Sample Data</button>
<button id="refresh-dashboard" type="button">Refresh Dashboard</button>
<textarea id="results-input" rows="6" class="monospace-input"></textarea>
<div class="button-row">
<button id="load-sample" type="button" class="btn">Load Sample Data</button>
<button id="refresh-dashboard" type="button" class="btn primary">
Refresh Dashboard
</button>
</div>
</div>
<div id="chart-container">
<h2>Result Distribution</h2>
</div>
<div id="chart-container">
<h3>Result Distribution</h3>
<canvas id="summary-chart" height="120"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
</div>
{% endblock %} {% block scripts %} {{ super() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const SUMMARY_FIELDS = [
{ key: "mean", label: "Mean" },
{ key: "median", label: "Median" },
@@ -272,6 +257,5 @@
}
initializeDashboard();
</script>
</body>
</html>
</script>
{% endblock %}

View File

@@ -1,50 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Process Parameters Input</title>
</head>
<body>
<h1>Enter Parameters for a Scenario</h1>
<form id="parameter-form">
<label
>Scenario ID:
<input
type="number"
name="scenario_id"
id="scenario_id"
required /></label
><br />
<label>Name: <input type="text" name="name" id="name" required /></label
><br />
<label
>Value:
<input
type="number"
name="value"
id="value"
step="any"
required /></label
><br />
<button type="submit">Add Parameter</button>
{% extends "base.html" %} {% block title %}Process Parameters · CalMiner{%
endblock %} {% block content %}
<section class="panel">
<h2>Scenario Parameters</h2>
{% if scenarios %}
<form id="parameter-form" class="form-grid">
<label>
<span>Scenario</span>
<select name="scenario_id" id="scenario_id">
{% for scenario in scenarios %}
<option value="{{ scenario.id }}">{{ scenario.name }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Name</span>
<input type="text" name="name" id="name" required />
</label>
<label>
<span>Value</span>
<input type="number" name="value" id="value" step="any" required />
</label>
<button type="submit" class="btn primary">Add Parameter</button>
</form>
<div id="result"></div>
<script>
document
.getElementById("parameter-form")
.addEventListener("submit", async (e) => {
e.preventDefault();
const scenario_id = document.getElementById("scenario_id").value;
const name = document.getElementById("name").value;
const value = parseFloat(document.getElementById("value").value);
const resp = await fetch("/api/parameters/", {
<p id="parameter-feedback" class="feedback" role="status"></p>
<div class="table-container">
<table id="parameter-table">
<thead>
<tr>
<th scope="col">Parameter</th>
<th scope="col">Value</th>
<th scope="col">Distribution</th>
<th scope="col">Details</th>
</tr>
</thead>
<tbody id="parameter-table-body"></tbody>
</table>
</div>
{% else %}
<p class="empty-state">
No scenarios available. Create a scenario before adding parameters.
</p>
{% endif %}
</section>
{% endblock %} {% block scripts %} {{ super() }}
<script id="parameters-data" type="application/json">
{{ parameters_by_scenario | tojson | safe }}
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const dataElement = document.getElementById("parameters-data");
const parsedData = dataElement
? JSON.parse(dataElement.textContent || "{}")
: {};
const parametersByScenario =
parsedData && typeof parsedData === "object" ? parsedData : {};
const form = document.getElementById("parameter-form");
const scenarioSelect = /** @type {HTMLSelectElement | null} */ (
document.getElementById("scenario_id")
);
const nameInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("name")
);
const valueInput = /** @type {HTMLInputElement | null} */ (
document.getElementById("value")
);
const feedback = document.getElementById("parameter-feedback");
const tableBody = document.getElementById("parameter-table-body");
const setFeedback = (message, variant) => {
if (!feedback) {
return;
}
feedback.textContent = message;
feedback.classList.remove("success", "error");
if (variant) {
feedback.classList.add(variant);
}
};
const renderTable = (scenarioId) => {
if (!tableBody) {
return;
}
tableBody.innerHTML = "";
const rows = parametersByScenario[String(scenarioId)] || [];
if (!rows.length) {
const emptyRow = document.createElement("tr");
emptyRow.id = "parameter-empty-state";
emptyRow.innerHTML =
'<td colspan="4">No parameters recorded for this scenario yet.</td>';
tableBody.appendChild(emptyRow);
return;
}
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${row.name}</td>
<td>${row.value}</td>
<td>${row.distribution_type ?? "—"}</td>
<td>${
row.distribution_parameters
? JSON.stringify(row.distribution_parameters)
: "—"
}</td>
`;
tableBody.appendChild(tr);
});
};
if (scenarioSelect) {
renderTable(scenarioSelect.value);
scenarioSelect.addEventListener("change", () =>
renderTable(scenarioSelect.value)
);
}
if (!form || !scenarioSelect || !nameInput || !valueInput) {
return;
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const scenarioId = scenarioSelect.value;
const payload = {
scenario_id: Number(scenarioId),
name: nameInput.value.trim(),
value: Number(valueInput.value),
};
if (!payload.name) {
setFeedback("Parameter name is required.", "error");
return;
}
if (!Number.isFinite(payload.value)) {
setFeedback("Enter a numeric value.", "error");
return;
}
const response = await fetch("/api/parameters/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scenario_id, name, value }),
body: JSON.stringify(payload),
});
const data = await resp.json();
document.getElementById("result").innerText = JSON.stringify(data);
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>
</body>
</html>
});
</script>
{% endblock %}

View File

@@ -1,36 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Scenario Management</title>
</head>
<body>
<h1>Create a New Scenario</h1>
<form id="scenario-form">
<label>Name: <input type="text" name="name" id="name" required /></label
><br />
<label
>Description:
<input type="text" name="description" id="description" /></label
><br />
<button type="submit">Create Scenario</button>
{% extends "base.html" %} {% block title %}Scenario Management · CalMiner{%
endblock %} {% block content %}
<section class="panel">
<h2>Create a New Scenario</h2>
<form id="scenario-form" class="form-grid">
<label>
<span>Name</span>
<input type="text" name="name" id="name" required />
</label>
<label>
<span>Description</span>
<input type="text" name="description" id="description" />
</label>
<button type="submit" class="btn primary">Create Scenario</button>
</form>
<div id="result"></div>
<script>
document
.getElementById("scenario-form")
.addEventListener("submit", async (e) => {
e.preventDefault();
const name = document.getElementById("name").value;
const description = document.getElementById("description").value;
const resp = await fetch("/api/scenarios/", {
<div class="table-container">
{% if scenarios %}
<table id="scenario-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody id="scenario-table-body">
{% for scenario in scenarios %}
<tr data-scenario-id="{{ scenario.id }}">
<td>{{ scenario.name }}</td>
<td>{{ scenario.description or "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p id="empty-state" class="empty-state">
No scenarios yet. Create one to get started.
</p>
<table id="scenario-table" class="hidden" aria-hidden="true">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody id="scenario-table-body"></tbody>
</table>
{% endif %}
</div>
</section>
{% endblock %} {% block scripts %} {{ super() }}
<script>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("scenario-form");
const nameInput = /** @type {HTMLInputElement} */ (
document.getElementById("name")
);
const descriptionInput = /** @type {HTMLInputElement} */ (
document.getElementById("description")
);
const table = document.getElementById("scenario-table");
const tableBody = document.getElementById("scenario-table-body");
const emptyState = document.getElementById("empty-state");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const payload = {
name: nameInput.value.trim(),
description: descriptionInput.value.trim() || null,
};
if (!payload.name) {
return;
}
const response = await fetch("/api/scenarios/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, description }),
body: JSON.stringify(payload),
});
const data = await resp.json();
document.getElementById("result").innerText = JSON.stringify(data);
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>
</body>
</html>
});
</script>
{% endblock %}

17
templates/base.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}CalMiner{% endblock %}</title>
<link rel="stylesheet" href="/static/css/main.css" />
{% block head_extra %}{% endblock %}
</head>
<body>
{% include "partials/base_header.html" %}
<main id="content" class="container">
{% block content %}{% endblock %}
</main>
{% include "partials/base_footer.html" %} {% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Consumption · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Consumption Tracking</h2>
<p>Placeholder for tracking scenario consumption metrics. Integration with APIs coming soon.</p>
</section>
{% endblock %}

8
templates/costs.html Normal file
View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Costs · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Costs</h2>
<p>This view will surface CAPEX and OPEX entries tied to scenarios. API wiring pending.</p>
</section>
{% endblock %}

8
templates/equipment.html Normal file
View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Equipment · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Equipment Inventory</h2>
<p>Placeholder for equipment CRUD interface. To be wired to `/api/equipment/` routes.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Maintenance · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Maintenance Log</h2>
<p>Placeholder for maintenance records management. Future work will surface CRUD flows tied to `/api/maintenance/`.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,5 @@
<footer class="site-footer">
<div class="container footer-inner">
<p>&copy; {{ current_year }} CalMiner. All rights reserved.</p>
</div>
</footer>

View File

@@ -0,0 +1,17 @@
<header class="site-header">
<div class="container header-inner">
<h1 class="site-title">CalMiner</h1>
<nav class="site-nav" aria-label="Primary navigation">
<a href="/ui/dashboard">Dashboard</a>
<a href="/ui/scenarios">Scenarios</a>
<a href="/ui/parameters">Parameters</a>
<a href="/ui/costs">Costs</a>
<a href="/ui/consumption">Consumption</a>
<a href="/ui/production">Production</a>
<a href="/ui/equipment">Equipment</a>
<a href="/ui/maintenance">Maintenance</a>
<a href="/ui/simulations">Simulations</a>
<a href="/ui/reporting">Reporting</a>
</nav>
</div>
</header>

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Production · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Production Output</h2>
<p>Placeholder for production metrics per scenario. Hook up to `/api/production/` endpoints.</p>
</section>
{% endblock %}

8
templates/reporting.html Normal file
View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Reporting · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Reporting</h2>
<p>Placeholder for aggregated KPI views connected to `/api/reporting/` endpoints.</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Simulations · CalMiner{% endblock %}
{% block content %}
<section class="panel">
<h2>Monte Carlo Simulations</h2>
<p>Placeholder for running simulations and reviewing outputs. Target integration: `/api/simulations/run`.</p>
</section>
{% endblock %}