Add job creation with UUID, STL upload/library, search, job log, TZ, and CLAUDE.md

- PrintJob: job_uuid (UUID4), customer, logs relationship
- JobLog model + migration 0004 (also includes job_logs table)
- POST /jobs: upload STL or select from library, auto-logs 'Job created'
- GET /jobs?q=: search by customer or filename across all statuses
- app/stl.py: STL_UPLOAD_DIR helper (from env, default /data/stl)
- docker-compose: named volume stl_files mounted at /data/stl
- .env.install: added STL_UPLOAD_DIR and TZ=Europe/Berlin
- CLAUDE.md: full project context for future sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-18 21:55:49 +02:00
parent c78f8c91cd
commit fe594e8675
10 changed files with 386 additions and 27 deletions

View File

@@ -2,28 +2,54 @@
{% block title %}Jobs — POPS{% endblock %}
{% block content %}
<h5 class="text-white mb-4">Print Jobs</h5>
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="text-white mb-0">Print Jobs</h5>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addJobModal">
<i class="bi bi-plus-lg me-1"></i>Add Job
</button>
</div>
{# ── Search bar ── #}
<form method="GET" action="/jobs" class="mb-3">
<div class="input-group input-group-sm" style="max-width:360px">
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
<input type="text" name="q" value="{{ q }}"
class="form-control bg-dark border-secondary text-white"
placeholder="Search by customer or filename…">
{% if q %}
<a href="/jobs" class="btn btn-outline-secondary">×</a>
{% endif %}
</div>
</form>
{% if q %}
<p class="text-secondary small mb-3">Results for <strong class="text-white">{{ q }}</strong></p>
{% endif %}
<div class="card">
{% if jobs %}
<table class="table table-dark table-hover mb-0 align-middle">
<table class="table table-dark table-hover mb-0 align-middle" style="font-size:.875rem">
<thead>
<tr class="text-secondary small">
<th class="fw-normal">Name</th>
<th class="fw-normal">UUID</th>
<th class="fw-normal">Customer</th>
<th class="fw-normal">File</th>
<th class="fw-normal">Printer</th>
<th class="fw-normal">Filament</th>
<th class="fw-normal">Material</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 class="font-monospace text-secondary" style="font-size:.75rem">
{{ j.job_uuid[:8] if j.job_uuid else '—' }}
</td>
<td class="text-white">{{ j.customer or '' }}</td>
<td class="text-secondary">{{ j.file_name or '—' }}</td>
<td class="text-secondary">{{ j.printer.name if j.printer else '—' }}</td>
<td class="text-secondary">
{% if j.filament %}{{ j.filament.material.value }}{% if j.filament.color %} · {{ j.filament.color }}{% endif %}{% else %}—{% endif %}
</td>
<td>
{% set s = j.status.value %}
@@ -34,19 +60,111 @@
{% 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>
<td class="text-secondary text-end">{{ j.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body text-secondary">No jobs yet.</div>
<div class="card-body text-secondary">
{% if q %}No jobs match "{{ q }}".{% else %}No jobs yet.{% endif %}
</div>
{% endif %}
</div>
{# ── Add Job Modal ── #}
<div class="modal fade" id="addJobModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-dark border-secondary">
<form method="POST" action="/jobs" enctype="multipart/form-data">
<div class="modal-header border-secondary">
<h6 class="modal-title text-white">Add Print Job</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body d-flex flex-column gap-3">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-secondary small">Printer <span class="text-danger">*</span></label>
<select name="printer_id" class="form-select bg-dark border-secondary text-white" required>
<option value="">— select —</option>
{% for p in printers %}
<option value="{{ p.id }}">{{ p.name }}{% if p.location %} ({{ p.location }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label text-secondary small">Customer</label>
<input type="text" name="customer" class="form-control bg-dark border-secondary text-white"
placeholder="Customer name or reference">
</div>
</div>
{# ── STL file ── #}
<div>
<label class="form-label text-secondary small">STL / File</label>
<ul class="nav nav-pills mb-2" id="stlTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active py-1 px-3" style="font-size:.8rem"
data-bs-toggle="pill" data-bs-target="#uploadPane" type="button">
<i class="bi bi-upload me-1"></i>Upload
</button>
</li>
<li class="nav-item">
<button class="nav-link py-1 px-3" style="font-size:.8rem"
data-bs-toggle="pill" data-bs-target="#libraryPane" type="button">
<i class="bi bi-folder2-open me-1"></i>Library
<span class="badge bg-secondary ms-1">{{ stl_files | length }}</span>
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="uploadPane">
<input type="file" name="stl_file"
class="form-control bg-dark border-secondary text-white"
accept=".stl,.3mf,.gcode">
</div>
<div class="tab-pane fade" id="libraryPane">
{% if stl_files %}
<select name="stl_select" class="form-select bg-dark border-secondary text-white">
<option value="">— select file —</option>
{% for f in stl_files %}
<option value="{{ f }}">{{ f }}</option>
{% endfor %}
</select>
{% else %}
<p class="text-secondary small mb-0">No files in library yet. Upload one first.</p>
{% endif %}
</div>
</div>
</div>
<div>
<label class="form-label text-secondary small">Material & Color</label>
<select name="filament_id" class="form-select bg-dark border-secondary text-white">
<option value="">— none —</option>
{% for f in filaments %}
<option value="{{ f.id }}">
{{ f.material.value }}{% if f.color %} · {{ f.color }}{% endif %}
{% if f.brand %} ({{ f.brand }}){% endif %}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="form-label text-secondary small">Notes</label>
<textarea name="notes" class="form-control bg-dark border-secondary text-white"
rows="2" placeholder="Optional notes"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm">Create Job</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}