Remove internal filament management, integrate FilamentDB, add job detail page

Filament removal:
- Deleted app/models/filament.py, routers/filaments.py, templates/filaments.html
- Migration 0009: drops filaments table and filament_id FK from print_jobs
- Removed Filaments from sidebar and all internal references

FilamentDB integration (read-only):
- app/filamentdb.py: GET /reports/stock with 5-min in-memory cache
- available_materials() / available_colors() populate job form dropdowns
- Falls back to static constants if FilamentDB unreachable
- Dashboard filament count now from FilamentDB rolls_in_stock
- FILAMENTDB_URL added to .env.install; httpx added to requirements

Job detail page (/jobs/{id}):
- Two cards: Job info (customer, material, printer, bracket, timing, cost)
  and File & Geometry (bounding box + volume from stl_cache)
- Log timeline with Add Entry modal
- Filename in jobs list links to detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-18 22:12:47 +02:00
parent 6868862d77
commit 1259a7294a
14 changed files with 294 additions and 138 deletions

View File

@@ -9,8 +9,8 @@ from sqlalchemy.orm import Session
from starlette.status import HTTP_303_SEE_OTHER
from app.db import get_db
from app.models.filament import Filament
from app.models.job_bracket import JobBracket
from app.models.stl_cache import StlCache
from app.models.job_log import JobLog
from app.models.print_job import JobStatus, PrintJob
from app.models.printer import Printer, PrinterStatus
@@ -19,6 +19,7 @@ from app.models.printer_type import PrinterType
from app.models.pricing import PricingConfig
from app.models.todo import Todo
from app.constants import COLORS, MATERIALS
from app.filamentdb import available_colors, available_materials, get_stock
from app.stl import get_stl_dir, list_stl_files
from app.stl_analysis import analyse, get_cache_map
@@ -40,7 +41,7 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
"active_jobs": db.query(PrintJob).filter(
PrintJob.status.in_([JobStatus.queued, JobStatus.printing])
).count(),
"total_filaments": db.query(Filament).count(),
"total_filaments": sum(r.get("rolls_in_stock", 0) for r in get_stock()),
"recent_jobs": db.query(PrintJob).order_by(PrintJob.created_at.desc()).limit(5).all(),
})
@@ -141,8 +142,8 @@ def jobs_page(request: Request, q: str = "", db: Session = Depends(get_db)):
"brackets": db.query(JobBracket).order_by(JobBracket.name).all(),
"stl_files": (stl_files := list_stl_files()),
"stl_cache": get_cache_map(stl_files, db),
"materials": MATERIALS,
"colors": COLORS,
"materials": available_materials() or MATERIALS,
"colors": available_colors() or COLORS,
})
@@ -205,6 +206,28 @@ async def create_job(
return RedirectResponse(url="/jobs", status_code=HTTP_303_SEE_OTHER)
@router.get("/jobs/{job_id}", response_class=HTMLResponse)
def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
from fastapi import HTTPException
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
stl = db.query(StlCache).filter_by(filename=job.file_name).first() if job.file_name else None
return templates.TemplateResponse("job_detail.html", {
"request": request,
"active": "jobs",
"job": job,
"stl": stl,
})
@router.post("/jobs/{job_id}/log")
def add_job_log(job_id: int, message: str = Form(...), db: Session = Depends(get_db)):
db.add(JobLog(job_id=job_id, message=message.strip()))
db.commit()
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
# ── Brackets ───────────────────────────────────────────────────────────────────
@router.get("/brackets", response_class=HTMLResponse)
@@ -241,8 +264,8 @@ def bracket_detail(bracket_id: int, request: Request, db: Session = Depends(get_
"filaments": db.query(Filament).order_by(Filament.material, Filament.color).all(),
"stl_files": (stl_files := list_stl_files()),
"stl_cache": get_cache_map(stl_files, db),
"materials": MATERIALS,
"colors": COLORS,
"materials": available_materials() or MATERIALS,
"colors": available_colors() or COLORS,
})
@@ -283,17 +306,6 @@ async def add_jobs_to_bracket(
return RedirectResponse(url=f"/brackets/{bracket_id}", status_code=HTTP_303_SEE_OTHER)
# ── Filaments ──────────────────────────────────────────────────────────────────
@router.get("/filaments", response_class=HTMLResponse)
def filaments_page(request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse("filaments.html", {
"request": request,
"active": "filaments",
"filaments": db.query(Filament).order_by(Filament.brand, Filament.material).all(),
})
# ── To-Do ──────────────────────────────────────────────────────────────────────
@router.get("/todos", response_class=HTMLResponse)