634 lines
24 KiB
Python
634 lines
24 KiB
Python
import math
|
||
import os
|
||
import re
|
||
import uuid as _uuid
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
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_
|
||
from sqlalchemy.orm import Session
|
||
from starlette.status import HTTP_303_SEE_OTHER
|
||
|
||
from app.db import get_db
|
||
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 # noqa: F401
|
||
from app.models.printer import Printer, PrinterStatus
|
||
from app.models.printer_log import PrinterLog
|
||
from app.models.printer_type import PrinterType
|
||
from app.models.pricing import PricingConfig
|
||
from app.vikunja import create_task as vikunja_create, delete_task as vikunja_delete, get_tasks as vikunja_tasks, update_task as vikunja_update
|
||
from app.constants import COLORS, MATERIALS
|
||
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_in_background, get_cache_map, is_computing, is_failed # noqa: F401
|
||
|
||
router = APIRouter()
|
||
templates = Jinja2Templates(directory="app/templates")
|
||
|
||
|
||
# ── Dashboard ──────────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/", response_class=HTMLResponse)
|
||
def dashboard(request: Request, db: Session = Depends(get_db)):
|
||
printers = db.query(Printer).order_by(Printer.name).all()
|
||
return templates.TemplateResponse("dashboard.html", {
|
||
"request": request,
|
||
"active": "dashboard",
|
||
"printers": printers,
|
||
"total_printers": len(printers),
|
||
"printing_count": sum(1 for p in printers if p.status == PrinterStatus.printing),
|
||
"active_jobs": db.query(PrintJob).filter(
|
||
PrintJob.status.in_([JobStatus.queued, JobStatus.printing])
|
||
).count(),
|
||
"total_filaments": sum(r.get("rolls_in_stock", 0) for r in get_stock()),
|
||
"filamentdb_url": os.environ.get("FILAMENTDB_URL", ""),
|
||
"recent_jobs": db.query(PrintJob).order_by(PrintJob.created_at.desc()).limit(5).all(),
|
||
})
|
||
|
||
|
||
# ── Printers ───────────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/printers", response_class=HTMLResponse)
|
||
def printers_page(request: Request, db: Session = Depends(get_db)):
|
||
return templates.TemplateResponse("printers.html", {
|
||
"request": request,
|
||
"active": "printers",
|
||
"printers": db.query(Printer).order_by(Printer.name).all(),
|
||
"printer_types": db.query(PrinterType).order_by(PrinterType.name).all(),
|
||
})
|
||
|
||
|
||
@router.post("/printers")
|
||
def create_printer(
|
||
name: str = Form(...),
|
||
location: str = Form(""),
|
||
type_id: str = Form(""),
|
||
bought_at: str = Form(""),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
from datetime import date
|
||
printer = Printer(
|
||
name=name.strip(),
|
||
location=location.strip() or None,
|
||
type_id=int(type_id) if type_id else None,
|
||
bought_at=date.fromisoformat(bought_at) if bought_at else None,
|
||
)
|
||
db.add(printer)
|
||
db.commit()
|
||
return RedirectResponse(url="/printers", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.get("/printers/{printer_id}", response_class=HTMLResponse)
|
||
def printer_detail(printer_id: int, request: Request, db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
printer = db.query(Printer).filter(Printer.id == printer_id).first()
|
||
if not printer:
|
||
raise HTTPException(status_code=404, detail="Printer not found")
|
||
current_job = (
|
||
db.query(PrintJob)
|
||
.filter(PrintJob.printer_id == printer_id, PrintJob.status == JobStatus.printing)
|
||
.order_by(PrintJob.started_at.desc())
|
||
.first()
|
||
)
|
||
return templates.TemplateResponse("printer_detail.html", {
|
||
"request": request,
|
||
"active": "printers",
|
||
"printer": printer,
|
||
"current_job": current_job,
|
||
})
|
||
|
||
|
||
@router.post("/printers/{printer_id}/log")
|
||
def add_printer_log(printer_id: int, message: str = Form(...), db: Session = Depends(get_db)):
|
||
db.add(PrinterLog(printer_id=printer_id, message=message.strip()))
|
||
db.commit()
|
||
return RedirectResponse(url=f"/printers/{printer_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
# ── Printer types ──────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/printer-types", response_class=HTMLResponse)
|
||
def printer_types_page(request: Request, db: Session = Depends(get_db)):
|
||
return templates.TemplateResponse("printer_types.html", {
|
||
"request": request,
|
||
"active": "printers",
|
||
"printer_types": db.query(PrinterType).order_by(PrinterType.name).all(),
|
||
})
|
||
|
||
|
||
@router.post("/printer-types")
|
||
def create_printer_type(
|
||
name: str = Form(...),
|
||
bed_x_mm: int = Form(...),
|
||
bed_y_mm: int = Form(...),
|
||
bed_z_mm: int = Form(...),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
db.add(PrinterType(name=name.strip(), bed_x_mm=bed_x_mm, bed_y_mm=bed_y_mm, bed_z_mm=bed_z_mm))
|
||
db.commit()
|
||
return RedirectResponse(url="/printer-types", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
# ── Jobs ───────────────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/jobs", response_class=HTMLResponse)
|
||
def jobs_page(request: Request, q: str = "", bracket_id: str = "", show_all: str = "", db: Session = Depends(get_db)):
|
||
query = db.query(PrintJob)
|
||
if q:
|
||
like = f"%{q}%"
|
||
query = query.filter(
|
||
or_(PrintJob.customer.ilike(like), PrintJob.file_name.ilike(like))
|
||
)
|
||
active_bracket = None
|
||
if bracket_id:
|
||
query = query.filter(PrintJob.bracket_id == int(bracket_id))
|
||
active_bracket = db.query(JobBracket).filter(JobBracket.id == int(bracket_id)).first()
|
||
|
||
# Hide finished jobs older than 24h unless show_all
|
||
finished = [JobStatus.done, JobStatus.failed, JobStatus.cancelled]
|
||
cutoff = datetime.now() - timedelta(hours=24)
|
||
hidden_count = 0
|
||
if not show_all:
|
||
hidden_count = query.filter(
|
||
PrintJob.status.in_(finished), PrintJob.created_at < cutoff
|
||
).count()
|
||
query = query.filter(
|
||
or_(~PrintJob.status.in_(finished), PrintJob.created_at >= cutoff)
|
||
)
|
||
|
||
# Sort: printing → queued → rest, then priority flag, then internal last, then oldest first
|
||
status_order = case(
|
||
(PrintJob.status == JobStatus.printing, 0),
|
||
(PrintJob.status == JobStatus.queued, 1),
|
||
else_=2,
|
||
)
|
||
internal_last = case((PrintJob.customer == "internal", 1), else_=0)
|
||
jobs = query.order_by(status_order, PrintJob.priority.desc(), internal_last, PrintJob.created_at.asc()).all()
|
||
|
||
stl_files = list_stl_files()
|
||
stl_cache = get_cache_map(stl_files, db)
|
||
|
||
bracket_gross_total = None
|
||
if active_bracket:
|
||
total = 0.0
|
||
has_any = False
|
||
for j in active_bracket.jobs:
|
||
cached = stl_cache.get(j.file_name)
|
||
if cached and cached.volume_ccm:
|
||
total += estimate(cached.volume_ccm, j.job_material)["gross_price"]
|
||
has_any = True
|
||
if has_any:
|
||
bracket_gross_total = round(total, 2)
|
||
|
||
return templates.TemplateResponse("jobs.html", {
|
||
"request": request,
|
||
"active": "jobs",
|
||
"jobs": jobs,
|
||
"q": q,
|
||
"bracket_id": bracket_id,
|
||
"show_all": bool(show_all),
|
||
"hidden_count": hidden_count,
|
||
"active_bracket": active_bracket,
|
||
"bracket_gross_total": bracket_gross_total,
|
||
"printers": db.query(Printer).order_by(Printer.name).all(),
|
||
"brackets": db.query(JobBracket).order_by(JobBracket.name).all(),
|
||
"stl_files": stl_files,
|
||
"stl_cache": stl_cache,
|
||
"materials": available_materials() or MATERIALS,
|
||
"colors": available_colors() or COLORS,
|
||
"spool_map": spool_counts(),
|
||
})
|
||
|
||
|
||
@router.post("/jobs")
|
||
async def create_job(
|
||
printer_id: str = Form(""),
|
||
customer: str = Form(""),
|
||
job_material: str = Form(""),
|
||
job_color: str = Form(""),
|
||
priority: str = Form(""),
|
||
bracket_id: str = Form(""),
|
||
amount: int = Form(1),
|
||
notes: str = Form(""),
|
||
stl_file: UploadFile = File(None),
|
||
stl_select: str = Form(""),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
filename = None
|
||
if stl_file and stl_file.filename:
|
||
filename = stl_file.filename
|
||
(get_stl_dir() / filename).write_bytes(await stl_file.read())
|
||
analyse(filename, db)
|
||
elif stl_select:
|
||
filename = stl_select
|
||
|
||
amount = max(1, amount)
|
||
customer_str = customer.strip() or "internal"
|
||
job_name = Path(filename).stem if filename else (customer_str or "Unnamed")
|
||
|
||
# Resolve bracket — auto-create one when printing multiple copies
|
||
effective_bracket_id = int(bracket_id) if bracket_id else None
|
||
if amount > 1 and not effective_bracket_id:
|
||
bracket_name = f"{job_name} ×{amount}"
|
||
if customer_str:
|
||
bracket_name = f"{customer_str} – {bracket_name}"
|
||
new_bracket = JobBracket(name=bracket_name, customer=customer_str)
|
||
db.add(new_bracket)
|
||
db.flush()
|
||
effective_bracket_id = new_bracket.id
|
||
|
||
for _ in range(amount):
|
||
job = PrintJob(
|
||
job_uuid=str(_uuid.uuid4()),
|
||
name=job_name,
|
||
file_name=filename,
|
||
printer_id=int(printer_id) if printer_id else None,
|
||
bracket_id=effective_bracket_id,
|
||
customer=customer_str,
|
||
job_material=job_material or None,
|
||
job_color=job_color or None,
|
||
priority=bool(priority),
|
||
notes=notes.strip() or None,
|
||
)
|
||
db.add(job)
|
||
db.flush()
|
||
db.add(JobLog(job_id=job.id, message="Job created"))
|
||
|
||
db.commit()
|
||
|
||
if effective_bracket_id:
|
||
return RedirectResponse(url=f"/jobs?bracket_id={effective_bracket_id}", status_code=HTTP_303_SEE_OTHER)
|
||
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, 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
|
||
if not file_path.exists():
|
||
file_missing = True
|
||
else:
|
||
stl = db.query(StlCache).filter_by(filename=job.file_name).first()
|
||
if stl is None and not job.file_name.endswith(".gcode"):
|
||
if is_failed(job.file_name):
|
||
file_missing = False # file exists, analysis just failed
|
||
elif not is_computing(job.file_name):
|
||
background_tasks.add_task(analyse_in_background, job.file_name)
|
||
computing = not is_failed(job.file_name)
|
||
|
||
estimation = estimate(stl.volume_ccm, job.job_material) if stl and stl.volume_ccm else None
|
||
return templates.TemplateResponse("job_detail.html", {
|
||
"request": request,
|
||
"active": "jobs",
|
||
"job": job,
|
||
"stl": stl,
|
||
"file_missing": file_missing,
|
||
"computing": computing,
|
||
"analysis_failed": job.file_name and is_failed(job.file_name),
|
||
"estimation": estimation,
|
||
"printers": db.query(Printer).order_by(Printer.name).all(),
|
||
"brackets": db.query(JobBracket).order_by(JobBracket.name).all(),
|
||
"materials": available_materials() or MATERIALS,
|
||
"colors": available_colors() or COLORS,
|
||
"all_statuses": [s.value for s in JobStatus],
|
||
"available_printers": db.query(Printer).filter(
|
||
Printer.status != PrinterStatus.printing
|
||
).order_by(Printer.name).all(),
|
||
})
|
||
|
||
|
||
@router.post("/jobs/{job_id}/edit")
|
||
def edit_job(
|
||
job_id: int,
|
||
customer: str = Form(""),
|
||
job_material: str = Form(""),
|
||
job_color: str = Form(""),
|
||
priority: str = Form(""),
|
||
bracket_id: str = Form(""),
|
||
printer_id: str = Form(""),
|
||
status: str = Form(""),
|
||
notes: str = Form(""),
|
||
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")
|
||
job.customer = customer.strip() or "internal"
|
||
job.job_material = job_material or None
|
||
job.job_color = job_color or None
|
||
job.priority = bool(priority)
|
||
job.bracket_id = int(bracket_id) if bracket_id else None
|
||
job.printer_id = int(printer_id) if printer_id else None
|
||
job.notes = notes.strip() or None
|
||
if status:
|
||
job.status = JobStatus(status)
|
||
db.add(JobLog(job_id=job.id, message=f"Job updated"))
|
||
db.commit()
|
||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/jobs/{job_id}/duplicate")
|
||
def duplicate_job(job_id: int, db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||
if not job:
|
||
raise HTTPException(404, "Job not found")
|
||
|
||
copy = PrintJob(
|
||
job_uuid=str(_uuid.uuid4()),
|
||
name=job.name,
|
||
file_name=job.file_name,
|
||
printer_id=None,
|
||
bracket_id=job.bracket_id,
|
||
customer=job.customer,
|
||
job_material=job.job_material,
|
||
job_color=job.job_color,
|
||
priority=job.priority,
|
||
status=JobStatus.queued,
|
||
notes=job.notes,
|
||
)
|
||
db.add(copy)
|
||
|
||
# If the original was in a bracket, extend the "×N" count in its name to match.
|
||
if job.bracket and job.bracket.name:
|
||
job.bracket.name = re.sub(
|
||
r"×(\d+)\s*$",
|
||
lambda m: f"×{int(m.group(1)) + 1}",
|
||
job.bracket.name,
|
||
)
|
||
|
||
db.flush()
|
||
db.add(JobLog(job_id=copy.id, message=f"Job created (duplicate of #{job.id})"))
|
||
db.commit()
|
||
|
||
if copy.bracket_id:
|
||
return RedirectResponse(url=f"/jobs?bracket_id={copy.bracket_id}", status_code=HTTP_303_SEE_OTHER)
|
||
return RedirectResponse(url="/jobs", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.get("/files/{filename}")
|
||
def download_file(filename: str):
|
||
from fastapi import HTTPException
|
||
stl_dir = get_stl_dir()
|
||
path = (stl_dir / filename).resolve()
|
||
if not str(path).startswith(str(stl_dir.resolve())):
|
||
raise HTTPException(400, "Invalid filename")
|
||
if not path.exists():
|
||
raise HTTPException(404, "File not found")
|
||
return FileResponse(path, filename=filename, media_type="application/octet-stream")
|
||
|
||
|
||
@router.post("/jobs/{job_id}/start")
|
||
def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||
if not job:
|
||
raise HTTPException(404, "Job not found")
|
||
printer = db.query(Printer).filter(Printer.id == printer_id).first()
|
||
if not printer:
|
||
raise HTTPException(404, "Printer not found")
|
||
|
||
job.status = JobStatus.printing
|
||
job.printer_id = printer_id
|
||
job.started_at = datetime.now()
|
||
printer.status = PrinterStatus.printing
|
||
|
||
db.add(JobLog(job_id=job_id, message=f"Job started on {printer.name}"))
|
||
|
||
# Log filament usage against matching AMS roll in FilamentDB
|
||
filament_low = False
|
||
if job.job_material and job.job_color:
|
||
stl = db.query(StlCache).filter_by(filename=job.file_name).first() if job.file_name else None
|
||
if stl and stl.volume_ccm:
|
||
weight_g = math.ceil(estimate(stl.volume_ccm, job.job_material)["weight_g"])
|
||
roll = find_matching_ams_roll(job.job_material, job.job_color)
|
||
if roll:
|
||
note = (f"POPS {job.job_uuid[:8] if job.job_uuid else job.id}: "
|
||
f"{job.file_name or job.name} on {printer.name}")
|
||
if log_print_on_roll(roll["roll_id"], weight_g, note):
|
||
db.add(JobLog(job_id=job_id, message=(
|
||
f"FilamentDB: logged {weight_g} g against roll #{roll['roll_id']} "
|
||
f"(AMS slot {roll['slot']} — {roll['material']} {roll['color']}, "
|
||
f"{roll['weight_remaining_g']} g remaining, least-full roll selected)"
|
||
)))
|
||
if roll["weight_remaining_g"] - weight_g < 100:
|
||
filament_low = True
|
||
else:
|
||
db.add(JobLog(job_id=job_id, message=f"FilamentDB: print log failed for roll #{roll['roll_id']}"))
|
||
else:
|
||
db.add(JobLog(job_id=job_id, message=(
|
||
f"FilamentDB: no {job.job_material} {job.job_color} roll found in AMS"
|
||
)))
|
||
|
||
db.commit()
|
||
if filament_low:
|
||
vikunja_create("Check filament status")
|
||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/jobs/{job_id}/cancel")
|
||
def cancel_job(job_id: int, db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||
if not job:
|
||
raise HTTPException(404, "Job not found")
|
||
if job.printer and job.status == JobStatus.printing:
|
||
job.printer.status = PrinterStatus.idle
|
||
job.status = JobStatus.cancelled
|
||
db.add(JobLog(job_id=job_id, message="Job cancelled"))
|
||
db.commit()
|
||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/jobs/{job_id}/stop")
|
||
def stop_job(job_id: int, db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||
if not job:
|
||
raise HTTPException(404, "Job not found")
|
||
|
||
printer_name = job.printer.name if job.printer else "unknown printer"
|
||
if job.printer:
|
||
job.printer.status = PrinterStatus.idle
|
||
|
||
job.status = JobStatus.queued
|
||
job.printer_id = None
|
||
job.started_at = None
|
||
|
||
db.add(JobLog(job_id=job_id, message=f"Job stopped (was on {printer_name})"))
|
||
db.commit()
|
||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/jobs/{job_id}/finish")
|
||
def finish_job(job_id: int, db: Session = Depends(get_db)):
|
||
from fastapi import HTTPException
|
||
job = db.query(PrintJob).filter(PrintJob.id == job_id).first()
|
||
if not job:
|
||
raise HTTPException(404, "Job not found")
|
||
|
||
now = datetime.now()
|
||
job.status = JobStatus.done
|
||
job.finished_at = now
|
||
if job.started_at:
|
||
job.duration_minutes = max(1, int((now - job.started_at).total_seconds() / 60))
|
||
|
||
printer_name = job.printer.name if job.printer else "unknown printer"
|
||
if job.printer:
|
||
job.printer.status = PrinterStatus.idle
|
||
|
||
db.add(JobLog(job_id=job_id, message=f"Job finished on {printer_name}"))
|
||
|
||
# Every 3rd completed print on this printer → cleaning reminder
|
||
cleaning_todo = False
|
||
if job.printer_id:
|
||
done_count = db.query(PrintJob).filter(
|
||
PrintJob.printer_id == job.printer_id,
|
||
PrintJob.status == JobStatus.done,
|
||
).count()
|
||
if (done_count + 1) % 3 == 0:
|
||
cleaning_todo = True
|
||
|
||
db.commit()
|
||
if cleaning_todo:
|
||
vikunja_create(f"Clean build plate on {printer_name}")
|
||
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/jobs/{job_id}/analyse")
|
||
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()
|
||
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)
|
||
|
||
|
||
@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)
|
||
def brackets_page(request: Request, db: Session = Depends(get_db)):
|
||
return templates.TemplateResponse("brackets.html", {
|
||
"request": request,
|
||
"active": "brackets",
|
||
"brackets": db.query(JobBracket).order_by(JobBracket.created_at.desc()).all(),
|
||
})
|
||
|
||
|
||
@router.post("/brackets")
|
||
def create_bracket(
|
||
name: str = Form(...),
|
||
customer: str = Form(""),
|
||
notes: str = Form(""),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
db.add(JobBracket(name=name.strip(), customer=customer.strip() or None, notes=notes.strip() or None))
|
||
db.commit()
|
||
return RedirectResponse(url="/brackets", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.get("/brackets/{bracket_id}")
|
||
def bracket_detail_redirect(bracket_id: int):
|
||
return RedirectResponse(url=f"/jobs?bracket_id={bracket_id}", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
# ── To-Do (Vikunja) ────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/todos", response_class=HTMLResponse)
|
||
def todos_page(request: Request):
|
||
tasks = vikunja_tasks()
|
||
tasks.sort(key=lambda t: (t.get("done", False), t.get("created", "")), reverse=False)
|
||
return templates.TemplateResponse("todos.html", {
|
||
"request": request,
|
||
"active": "todos",
|
||
"todos": tasks,
|
||
})
|
||
|
||
|
||
@router.post("/todos")
|
||
def create_todo(title: str = Form(...)):
|
||
vikunja_create(title.strip())
|
||
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/todos/{task_id}/done")
|
||
def done_todo(task_id: int):
|
||
vikunja_update(task_id, done=True)
|
||
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
@router.post("/todos/{task_id}/delete")
|
||
def delete_todo(task_id: int):
|
||
vikunja_delete(task_id)
|
||
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
|
||
|
||
|
||
# ── Pricing ────────────────────────────────────────────────────────────────────
|
||
|
||
@router.get("/pricing", response_class=HTMLResponse)
|
||
def pricing_page(request: Request, db: Session = Depends(get_db)):
|
||
return templates.TemplateResponse("pricing.html", {
|
||
"request": request,
|
||
"active": "pricing",
|
||
"configs": db.query(PricingConfig).order_by(PricingConfig.valid_from.desc()).all(),
|
||
})
|