Add Jinja2 web UI with Bootstrap 5 dark theme
- app/templates/: base layout with sidebar nav + 6 pages (dashboard, printers, jobs, filaments, todos, pricing) - app/routers/ui.py: HTML routes querying the DB live - API routes moved to /api/* prefix - requirements: jinja2, python-multipart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
65
app/templates/base.html
Normal file
65
app/templates/base.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}POPS{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #0d1117; }
|
||||
#sidebar {
|
||||
width: 230px;
|
||||
min-height: 100vh;
|
||||
background: #010409;
|
||||
border-right: 1px solid #21262d;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#sidebar .brand { border-bottom: 1px solid #21262d; }
|
||||
#sidebar .nav-link {
|
||||
color: #8b949e;
|
||||
border-radius: 6px;
|
||||
padding: .4rem .75rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
#sidebar .nav-link i { width: 18px; }
|
||||
#sidebar .nav-link:hover { color: #e6edf3; background: #161b22; }
|
||||
#sidebar .nav-link.active { color: #e6edf3; background: #21262d; font-weight: 500; }
|
||||
#content { min-height: 100vh; }
|
||||
.card { border-color: #21262d !important; background: #161b22 !important; }
|
||||
.table-dark { --bs-table-bg: transparent; }
|
||||
.table > :not(caption) > * > * { border-color: #21262d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex">
|
||||
|
||||
<nav id="sidebar" class="p-3 d-flex flex-column gap-3">
|
||||
<a href="/" class="brand text-decoration-none pb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-printer-fill text-primary fs-5"></i>
|
||||
<div>
|
||||
<div class="text-white fw-semibold lh-sm">POPS</div>
|
||||
<div class="text-secondary lh-sm" style="font-size:.68rem;letter-spacing:.03em">PRINT FARM MANAGER</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<ul class="nav flex-column gap-1">
|
||||
<li><a href="/" class="nav-link {% if active == 'dashboard' %}active{% endif %}"><i class="bi bi-speedometer2 me-2"></i>Dashboard</a></li>
|
||||
<li><a href="/printers" class="nav-link {% if active == 'printers' %}active{% endif %}"><i class="bi bi-printer me-2"></i>Printers</a></li>
|
||||
<li><a href="/jobs" class="nav-link {% if active == 'jobs' %}active{% endif %}"><i class="bi bi-list-task me-2"></i>Jobs</a></li>
|
||||
<li><a href="/filaments" class="nav-link {% if active == 'filaments' %}active{% endif %}"><i class="bi bi-record-circle me-2"></i>Filaments</a></li>
|
||||
<li><a href="/todos" class="nav-link {% if active == 'todos' %}active{% endif %}"><i class="bi bi-check2-square me-2"></i>To-Do</a></li>
|
||||
<li><a href="/pricing" class="nav-link {% if active == 'pricing' %}active{% endif %}"><i class="bi bi-currency-euro me-2"></i>Pricing</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main id="content" class="flex-grow-1 p-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
106
app/templates/dashboard.html
Normal file
106
app/templates/dashboard.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">Dashboard</h5>
|
||||
|
||||
{# ── Stats row ── #}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card p-3">
|
||||
<div class="text-secondary small mb-1">Printers</div>
|
||||
<div class="fs-2 fw-bold text-white">{{ total_printers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card p-3">
|
||||
<div class="text-secondary small mb-1">Printing Now</div>
|
||||
<div class="fs-2 fw-bold text-primary">{{ printing_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card p-3">
|
||||
<div class="text-secondary small mb-1">Active Jobs</div>
|
||||
<div class="fs-2 fw-bold text-warning">{{ active_jobs }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card p-3">
|
||||
<div class="text-secondary small mb-1">Filament Spools</div>
|
||||
<div class="fs-2 fw-bold text-white">{{ total_filaments }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
{# ── Printer overview ── #}
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-secondary small text-uppercase fw-normal" style="border-color:#21262d">Printers</div>
|
||||
{% if printers %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<tbody>
|
||||
{% for p in printers %}
|
||||
<tr>
|
||||
<td>{{ p.name }}</td>
|
||||
<td class="text-secondary small">{{ p.model or '—' }}</td>
|
||||
<td class="text-end">
|
||||
{% set s = p.status.value %}
|
||||
{% if s == 'printing' %}<span class="badge bg-primary">printing</span>
|
||||
{% elif s == 'idle' %}<span class="badge bg-success">idle</span>
|
||||
{% elif s == 'error' %}<span class="badge bg-danger">error</span>
|
||||
{% else %}<span class="badge bg-secondary">offline</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No printers configured yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Recent jobs ── #}
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-secondary small text-uppercase fw-normal" style="border-color:#21262d">Recent Jobs</div>
|
||||
{% if recent_jobs %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr class="text-secondary small">
|
||||
<th class="fw-normal">Name</th>
|
||||
<th class="fw-normal">Printer</th>
|
||||
<th class="fw-normal">Status</th>
|
||||
<th class="fw-normal text-end">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for j in recent_jobs %}
|
||||
<tr>
|
||||
<td>{{ j.name }}</td>
|
||||
<td class="text-secondary small">{{ j.printer.name if j.printer else '—' }}</td>
|
||||
<td>
|
||||
{% set s = j.status.value %}
|
||||
{% if s == 'printing' %}<span class="badge bg-primary">printing</span>
|
||||
{% elif s == 'done' %}<span class="badge bg-success">done</span>
|
||||
{% elif s == 'failed' %}<span class="badge bg-danger">failed</span>
|
||||
{% elif s == 'cancelled' %}<span class="badge bg-warning text-dark">cancelled</span>
|
||||
{% else %}<span class="badge bg-secondary">queued</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small text-end">{{ j.created_at.strftime('%d.%m %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No jobs yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
app/templates/filaments.html
Normal file
52
app/templates/filaments.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Filaments — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">Filament Inventory</h5>
|
||||
<div class="card">
|
||||
{% if filaments %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr class="text-secondary small">
|
||||
<th class="fw-normal">Brand</th>
|
||||
<th class="fw-normal">Material</th>
|
||||
<th class="fw-normal">Color</th>
|
||||
<th class="fw-normal">Remaining</th>
|
||||
<th class="fw-normal text-end">Price / kg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in filaments %}
|
||||
{% set pct = (f.weight_remaining_g / f.weight_total_g * 100) | int if f.weight_total_g else 0 %}
|
||||
<tr>
|
||||
<td class="fw-medium text-white">{{ f.brand or '—' }}</td>
|
||||
<td><span class="badge bg-secondary">{{ f.material.value }}</span></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if f.color_hex %}
|
||||
<span style="display:inline-block;width:12px;height:12px;border-radius:3px;background:{{ f.color_hex }};border:1px solid #444"></span>
|
||||
{% endif %}
|
||||
{{ f.color or '—' }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="min-width:160px">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="progress flex-grow-1" style="height:6px">
|
||||
<div class="progress-bar {% if pct < 20 %}bg-danger{% elif pct < 50 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
style="width:{{ pct }}%"></div>
|
||||
</div>
|
||||
<span class="text-secondary small" style="min-width:70px">{{ f.weight_remaining_g | int }}g / {{ f.weight_total_g | int }}g</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-secondary small text-end">
|
||||
{% if f.price_per_kg_eur %}€ {{ "%.2f"|format(f.price_per_kg_eur) }}{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No filaments in inventory yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
app/templates/jobs.html
Normal file
52
app/templates/jobs.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Jobs — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">Print Jobs</h5>
|
||||
<div class="card">
|
||||
{% if jobs %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr class="text-secondary small">
|
||||
<th class="fw-normal">Name</th>
|
||||
<th class="fw-normal">Printer</th>
|
||||
<th class="fw-normal">Filament</th>
|
||||
<th class="fw-normal">Status</th>
|
||||
<th class="fw-normal">Duration</th>
|
||||
<th class="fw-normal text-end">Cost</th>
|
||||
<th class="fw-normal text-end">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for j in jobs %}
|
||||
<tr>
|
||||
<td class="fw-medium text-white">{{ j.name }}</td>
|
||||
<td class="text-secondary small">{{ j.printer.name if j.printer else '—' }}</td>
|
||||
<td class="text-secondary small">
|
||||
{% if j.filament %}{{ j.filament.material.value }} {{ j.filament.color or '' }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set s = j.status.value %}
|
||||
{% if s == 'printing' %}<span class="badge bg-primary">printing</span>
|
||||
{% elif s == 'done' %}<span class="badge bg-success">done</span>
|
||||
{% elif s == 'failed' %}<span class="badge bg-danger">failed</span>
|
||||
{% elif s == 'cancelled' %}<span class="badge bg-warning text-dark">cancelled</span>
|
||||
{% else %}<span class="badge bg-secondary">queued</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small">
|
||||
{% if j.duration_minutes %}{{ j.duration_minutes }} min{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small text-end">
|
||||
{% if j.cost_eur %}€ {{ "%.2f"|format(j.cost_eur) }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small text-end">{{ j.created_at.strftime('%d.%m %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No jobs yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
app/templates/pricing.html
Normal file
30
app/templates/pricing.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Pricing — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">Pricing Configuration</h5>
|
||||
{% if configs %}
|
||||
<div class="row g-3">
|
||||
{% for c in configs %}
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="card p-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<span class="fw-semibold text-white">{{ c.name }}</span>
|
||||
<span class="text-secondary small">from {{ c.valid_from.strftime('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
<table class="table table-dark table-sm mb-0" style="font-size:.85rem">
|
||||
<tbody>
|
||||
<tr><td class="text-secondary border-0">Electricity</td><td class="text-end border-0">€ {{ "%.4f"|format(c.electricity_kwh_eur) }} / kWh</td></tr>
|
||||
<tr><td class="text-secondary">Printer draw</td><td class="text-end">{{ c.printer_watt }} W</td></tr>
|
||||
<tr><td class="text-secondary">Hourly rate</td><td class="text-end">€ {{ "%.2f"|format(c.hourly_rate_eur) }} / h</td></tr>
|
||||
<tr><td class="text-secondary">Markup</td><td class="text-end">{{ c.markup_percent }} %</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card"><div class="card-body text-secondary">No pricing config yet.</div></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
41
app/templates/printers.html
Normal file
41
app/templates/printers.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Printers — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">Printers</h5>
|
||||
<div class="card">
|
||||
{% if printers %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr class="text-secondary small">
|
||||
<th class="fw-normal">Name</th>
|
||||
<th class="fw-normal">Model</th>
|
||||
<th class="fw-normal">Location</th>
|
||||
<th class="fw-normal">Status</th>
|
||||
<th class="fw-normal">Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in printers %}
|
||||
<tr>
|
||||
<td class="fw-medium text-white">{{ p.name }}</td>
|
||||
<td class="text-secondary">{{ p.model or '—' }}</td>
|
||||
<td class="text-secondary">{{ p.location or '—' }}</td>
|
||||
<td>
|
||||
{% set s = p.status.value %}
|
||||
{% if s == 'printing' %}<span class="badge bg-primary">printing</span>
|
||||
{% elif s == 'idle' %}<span class="badge bg-success">idle</span>
|
||||
{% elif s == 'error' %}<span class="badge bg-danger">error</span>
|
||||
{% else %}<span class="badge bg-secondary">offline</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small">{{ p.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No printers configured yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
app/templates/todos.html
Normal file
40
app/templates/todos.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}To-Do — POPS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="text-white mb-4">To-Do</h5>
|
||||
<div class="card">
|
||||
{% if todos %}
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr class="text-secondary small">
|
||||
<th class="fw-normal">Task</th>
|
||||
<th class="fw-normal">Printer</th>
|
||||
<th class="fw-normal text-center">Done</th>
|
||||
<th class="fw-normal text-end">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in todos %}
|
||||
<tr class="{% if t.done %}opacity-50{% endif %}">
|
||||
<td class="{% if t.done %}text-decoration-line-through text-secondary{% else %}text-white{% endif %}">
|
||||
{{ t.title }}
|
||||
</td>
|
||||
<td class="text-secondary small">{{ t.printer.name if t.printer else '—' }}</td>
|
||||
<td class="text-center">
|
||||
{% if t.done %}
|
||||
<i class="bi bi-check-circle-fill text-success"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-circle text-secondary"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-secondary small text-end">{{ t.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-secondary">No to-do items yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user