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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
17
main.py
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
144
routes/ui.py
144
routes/ui.py
@@ -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
209
static/css/main.css
Normal 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;
|
||||
}
|
||||
@@ -1,19 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>CalMiner Dashboard</title>
|
||||
{% extends "base.html" %} {% block title %}Dashboard · CalMiner{% endblock %} {%
|
||||
block head_extra %} {{ super() }}
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 2rem;
|
||||
background-color: #f4f5f7;
|
||||
color: #1f2933;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.summary-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
@@ -51,34 +38,32 @@
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simulation Results Dashboard</h1>
|
||||
{% endblock %} {% block content %}
|
||||
<h2>Simulation Results Dashboard</h2>
|
||||
<div class="summary-card">
|
||||
<h2>Summary Statistics</h2>
|
||||
<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>
|
||||
<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>
|
||||
<h3>Result Distribution</h3>
|
||||
<canvas id="summary-chart" height="120"></canvas>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ super() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
const SUMMARY_FIELDS = [
|
||||
@@ -273,5 +258,4 @@
|
||||
|
||||
initializeDashboard();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,50 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Process Parameters Input</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Enter Parameters for a Scenario</h1>
|
||||
<form id="parameter-form">
|
||||
<label
|
||||
>Scenario ID:
|
||||
<input
|
||||
type="number"
|
||||
name="scenario_id"
|
||||
id="scenario_id"
|
||||
required /></label
|
||||
><br />
|
||||
<label>Name: <input type="text" name="name" id="name" required /></label
|
||||
><br />
|
||||
<label
|
||||
>Value:
|
||||
<input
|
||||
type="number"
|
||||
name="value"
|
||||
id="value"
|
||||
step="any"
|
||||
required /></label
|
||||
><br />
|
||||
<button type="submit">Add Parameter</button>
|
||||
{% 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>
|
||||
<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
|
||||
.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/", {
|
||||
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),
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
const data = await resp.json();
|
||||
document.getElementById("result").innerText = JSON.stringify(data);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,36 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Scenario Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Create a New Scenario</h1>
|
||||
<form id="scenario-form">
|
||||
<label>Name: <input type="text" name="name" id="name" required /></label
|
||||
><br />
|
||||
<label
|
||||
>Description:
|
||||
<input type="text" name="description" id="description" /></label
|
||||
><br />
|
||||
<button type="submit">Create Scenario</button>
|
||||
{% 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>
|
||||
<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
|
||||
.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/", {
|
||||
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),
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
const data = await resp.json();
|
||||
document.getElementById("result").innerText = JSON.stringify(data);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
17
templates/base.html
Normal file
17
templates/base.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}CalMiner{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css" />
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% include "partials/base_header.html" %}
|
||||
<main id="content" class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% include "partials/base_footer.html" %} {% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
8
templates/consumption.html
Normal file
8
templates/consumption.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Consumption · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Consumption Tracking</h2>
|
||||
<p>Placeholder for tracking scenario consumption metrics. Integration with APIs coming soon.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/costs.html
Normal file
8
templates/costs.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Costs · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Costs</h2>
|
||||
<p>This view will surface CAPEX and OPEX entries tied to scenarios. API wiring pending.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/equipment.html
Normal file
8
templates/equipment.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Equipment · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Equipment Inventory</h2>
|
||||
<p>Placeholder for equipment CRUD interface. To be wired to `/api/equipment/` routes.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/maintenance.html
Normal file
8
templates/maintenance.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Maintenance · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Maintenance Log</h2>
|
||||
<p>Placeholder for maintenance records management. Future work will surface CRUD flows tied to `/api/maintenance/`.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
5
templates/partials/base_footer.html
Normal file
5
templates/partials/base_footer.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>© {{ current_year }} CalMiner. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
17
templates/partials/base_header.html
Normal file
17
templates/partials/base_header.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<h1 class="site-title">CalMiner</h1>
|
||||
<nav class="site-nav" aria-label="Primary navigation">
|
||||
<a href="/ui/dashboard">Dashboard</a>
|
||||
<a href="/ui/scenarios">Scenarios</a>
|
||||
<a href="/ui/parameters">Parameters</a>
|
||||
<a href="/ui/costs">Costs</a>
|
||||
<a href="/ui/consumption">Consumption</a>
|
||||
<a href="/ui/production">Production</a>
|
||||
<a href="/ui/equipment">Equipment</a>
|
||||
<a href="/ui/maintenance">Maintenance</a>
|
||||
<a href="/ui/simulations">Simulations</a>
|
||||
<a href="/ui/reporting">Reporting</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
8
templates/production.html
Normal file
8
templates/production.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Production · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Production Output</h2>
|
||||
<p>Placeholder for production metrics per scenario. Hook up to `/api/production/` endpoints.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/reporting.html
Normal file
8
templates/reporting.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reporting · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Reporting</h2>
|
||||
<p>Placeholder for aggregated KPI views connected to `/api/reporting/` endpoints.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
templates/simulations.html
Normal file
8
templates/simulations.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Simulations · CalMiner{% endblock %}
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h2>Monte Carlo Simulations</h2>
|
||||
<p>Placeholder for running simulations and reviewing outputs. Target integration: `/api/simulations/run`.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user