Background STL analysis with computing spinner; manual geometry override

- analyse_in_background() runs in FastAPI BackgroundTasks with its own
  DB session; _in_progress set prevents duplicate concurrent analyses
- Job detail shows a spinner + auto-reloads every 3s while computing
- 'Compute now' button also triggers background analysis
- StlCache.manually_set flag (migration 0011): entries marked manual are
  never overwritten by auto-analysis
- 'Override values' collapsible form on job detail lets user set
  bounding box and volume directly from slicer data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-19 11:07:44 +02:00
parent fc13d905db
commit 6a1f146d8e
5 changed files with 126 additions and 9 deletions

View File

@@ -4,7 +4,7 @@ import uuid as _uuid
from datetime import datetime, timedelta
from pathlib import Path
from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, Request, UploadFile
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import case, or_
@@ -26,7 +26,7 @@ from app.estimation import estimate # also used in start_job
from app.filamentdb import (available_colors, available_materials, find_matching_ams_roll,
get_stock, log_print_on_roll, spool_counts)
from app.stl import get_stl_dir, list_stl_files
from app.stl_analysis import analyse, get_cache_map # noqa: F401 used in job_detail route
from app.stl_analysis import analyse_in_background, get_cache_map, is_computing # noqa: F401
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@@ -260,13 +260,14 @@ async def create_job(
@router.get("/jobs/{job_id}", response_class=HTMLResponse)
def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
def job_detail(job_id: int, request: Request, background_tasks: BackgroundTasks, 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")
file_missing = False
computing = False
stl = None
if job.file_name:
file_path = get_stl_dir() / job.file_name
@@ -275,7 +276,9 @@ def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
else:
stl = db.query(StlCache).filter_by(filename=job.file_name).first()
if stl is None and not job.file_name.endswith(".gcode"):
stl = analyse(job.file_name, db)
if not is_computing(job.file_name):
background_tasks.add_task(analyse_in_background, job.file_name)
computing = True
estimation = estimate(stl.volume_ccm, job.job_material) if stl and stl.volume_ccm else None
return templates.TemplateResponse("job_detail.html", {
@@ -284,6 +287,7 @@ def job_detail(job_id: int, request: Request, db: Session = Depends(get_db)):
"job": job,
"stl": stl,
"file_missing": file_missing,
"computing": computing,
"estimation": estimation,
"printers": db.query(Printer).order_by(Printer.name).all(),
"brackets": db.query(JobBracket).order_by(JobBracket.name).all(),
@@ -457,14 +461,47 @@ def finish_job(job_id: int, db: Session = Depends(get_db)):
@router.post("/jobs/{job_id}/analyse")
def analyse_job_file(job_id: int, db: Session = Depends(get_db)):
def analyse_job_file(job_id: int, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
from fastapi import HTTPException
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
if not job or not job.file_name:
raise HTTPException(status_code=404, detail="Job or file not found")
db.query(StlCache).filter_by(filename=job.file_name).delete()
db.commit()
analyse(job.file_name, db)
background_tasks.add_task(analyse_in_background, job.file_name)
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
@router.post("/jobs/{job_id}/geometry")
def set_geometry(
job_id: int,
volume_ccm: float = Form(...),
cube_x: float = Form(...),
cube_y: float = Form(...),
cube_z: float = Form(...),
db: Session = Depends(get_db),
):
from fastapi import HTTPException
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
if not job or not job.file_name:
raise HTTPException(status_code=404, detail="Job or file not found")
stl = db.query(StlCache).filter_by(filename=job.file_name).first()
if stl:
stl.volume_ccm = volume_ccm
stl.cube_x = cube_x
stl.cube_y = cube_y
stl.cube_z = cube_z
stl.manually_set = True
else:
db.add(StlCache(
filename=job.file_name,
volume_ccm=volume_ccm,
cube_x=cube_x,
cube_y=cube_y,
cube_z=cube_z,
manually_set=True,
))
db.commit()
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)