From 750edbbcec89798809c23a5658911d4fd5d1443e Mon Sep 17 00:00:00 2001 From: Martin Hohenberg Date: Thu, 18 Jun 2026 21:46:26 +0200 Subject: [PATCH] Add printer types with bed volume + add-printer UI - PrinterType model: name, bed_x_mm/y/z, linked to Printer via type_id - Migration 0002: creates printer_types, adds type_id to printers, drops model - /printer-types page: table + Add Type modal form - /printers page: Add Printer modal with type dropdown (disabled if no types) - API: GET/POST/DELETE /api/printer-types Co-Authored-By: Claude Sonnet 4.6 --- alembic/versions/0002_printer_types.py | 40 ++++++++++++ app/main.py | 3 +- app/models/__init__.py | 3 +- app/models/printer.py | 5 +- app/models/printer_type.py | 19 ++++++ app/routers/printer_types.py | 39 ++++++++++++ app/routers/ui.py | 47 +++++++++++++- app/templates/printer_types.html | 86 ++++++++++++++++++++++++++ app/templates/printers.html | 79 +++++++++++++++++++++-- 9 files changed, 309 insertions(+), 12 deletions(-) create mode 100644 alembic/versions/0002_printer_types.py create mode 100644 app/models/printer_type.py create mode 100644 app/routers/printer_types.py create mode 100644 app/templates/printer_types.html diff --git a/alembic/versions/0002_printer_types.py b/alembic/versions/0002_printer_types.py new file mode 100644 index 0000000..e1c882d --- /dev/null +++ b/alembic/versions/0002_printer_types.py @@ -0,0 +1,40 @@ +"""printer_types table and link to printers + +Revision ID: 0002 +Revises: 0001 +Create Date: 2026-06-18 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0002" +down_revision: Union[str, None] = "0001" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "printer_types", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("bed_x_mm", sa.Integer(), nullable=False), + sa.Column("bed_y_mm", sa.Integer(), nullable=False), + sa.Column("bed_z_mm", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name"), + ) + op.add_column("printers", sa.Column("type_id", sa.Integer(), nullable=True)) + op.create_foreign_key("fk_printer_type", "printers", "printer_types", ["type_id"], ["id"]) + op.drop_column("printers", "model") + + +def downgrade() -> None: + op.drop_constraint("fk_printer_type", "printers", type_="foreignkey") + op.drop_column("printers", "type_id") + op.add_column("printers", sa.Column("model", sa.String(length=100), nullable=True)) + op.drop_table("printer_types") diff --git a/app/main.py b/app/main.py index fda4b98..9436af0 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from app.db import check_db, run_migrations -from app.routers import filaments, pricing, print_jobs, printers, todos +from app.routers import filaments, pricing, print_jobs, printer_types, printers, todos from app.routers import ui logging.basicConfig( @@ -39,6 +39,7 @@ async def unhandled_exception(request: Request, exc: Exception): # JSON API +app.include_router(printer_types.router, prefix="/api") app.include_router(printers.router, prefix="/api") app.include_router(filaments.router, prefix="/api") app.include_router(print_jobs.router, prefix="/api") diff --git a/app/models/__init__.py b/app/models/__init__.py index c76a13d..ee9945f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,6 +3,7 @@ from app.models.filament import Filament from app.models.pricing import PricingConfig from app.models.print_job import PrintJob from app.models.printer import Printer +from app.models.printer_type import PrinterType from app.models.todo import Todo -__all__ = ["Base", "Printer", "Filament", "PrintJob", "Todo", "PricingConfig"] +__all__ = ["Base", "Printer", "PrinterType", "Filament", "PrintJob", "Todo", "PricingConfig"] diff --git a/app/models/printer.py b/app/models/printer.py index d0a9192..be8c19b 100644 --- a/app/models/printer.py +++ b/app/models/printer.py @@ -1,7 +1,7 @@ import enum from datetime import datetime -from sqlalchemy import DateTime, Enum, String, func +from sqlalchemy import DateTime, Enum, ForeignKey, String, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base @@ -19,8 +19,8 @@ class Printer(Base): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(100)) - model: Mapped[str | None] = mapped_column(String(100)) location: Mapped[str | None] = mapped_column(String(100)) + type_id: Mapped[int | None] = mapped_column(ForeignKey("printer_types.id")) status: Mapped[PrinterStatus] = mapped_column( Enum(PrinterStatus), default=PrinterStatus.offline ) @@ -29,5 +29,6 @@ class Printer(Base): DateTime, server_default=func.now(), onupdate=func.now() ) + printer_type: Mapped["PrinterType | None"] = relationship(back_populates="printers") jobs: Mapped[list["PrintJob"]] = relationship(back_populates="printer") todos: Mapped[list["Todo"]] = relationship(back_populates="printer") diff --git a/app/models/printer_type.py b/app/models/printer_type.py new file mode 100644 index 0000000..231dc4d --- /dev/null +++ b/app/models/printer_type.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.models.base import Base + + +class PrinterType(Base): + __tablename__ = "printer_types" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(100), unique=True) + bed_x_mm: Mapped[int] = mapped_column(Integer) + bed_y_mm: Mapped[int] = mapped_column(Integer) + bed_z_mm: Mapped[int] = mapped_column(Integer) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + + printers: Mapped[list["Printer"]] = relationship(back_populates="printer_type") diff --git a/app/routers/printer_types.py b/app/routers/printer_types.py new file mode 100644 index 0000000..58d9e20 --- /dev/null +++ b/app/routers/printer_types.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from app.db import get_db +from app.models.printer_type import PrinterType + +router = APIRouter(prefix="/printer-types", tags=["printer-types"]) + + +class PrinterTypeCreate(BaseModel): + name: str + bed_x_mm: int + bed_y_mm: int + bed_z_mm: int + + +@router.get("/") +def list_printer_types(db: Session = Depends(get_db)): + return [t.to_dict() for t in db.query(PrinterType).order_by(PrinterType.name).all()] + + +@router.post("/") +def create_printer_type(data: PrinterTypeCreate, db: Session = Depends(get_db)): + pt = PrinterType(**data.model_dump()) + db.add(pt) + db.commit() + db.refresh(pt) + return pt.to_dict() + + +@router.delete("/{type_id}") +def delete_printer_type(type_id: int, db: Session = Depends(get_db)): + pt = db.query(PrinterType).filter(PrinterType.id == type_id).first() + if not pt: + raise HTTPException(status_code=404, detail="Printer type not found") + db.delete(pt) + db.commit() + return {"ok": True} diff --git a/app/routers/ui.py b/app/routers/ui.py index 0d56313..63287ea 100644 --- a/app/routers/ui.py +++ b/app/routers/ui.py @@ -1,12 +1,14 @@ -from fastapi import APIRouter, Depends, Request -from fastapi.responses import HTMLResponse +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates 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.print_job import JobStatus, PrintJob from app.models.printer import Printer, PrinterStatus +from app.models.printer_type import PrinterType from app.models.pricing import PricingConfig from app.models.todo import Todo @@ -37,9 +39,50 @@ def printers_page(request: Request, db: Session = Depends(get_db)): "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(""), + db: Session = Depends(get_db), +): + printer = Printer( + name=name.strip(), + location=location.strip() or None, + type_id=int(type_id) if type_id else None, + ) + db.add(printer) + db.commit() + return RedirectResponse(url="/printers", status_code=HTTP_303_SEE_OTHER) + + +@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), +): + pt = PrinterType(name=name.strip(), bed_x_mm=bed_x_mm, bed_y_mm=bed_y_mm, bed_z_mm=bed_z_mm) + db.add(pt) + db.commit() + return RedirectResponse(url="/printer-types", status_code=HTTP_303_SEE_OTHER) + + @router.get("/jobs", response_class=HTMLResponse) def jobs_page(request: Request, db: Session = Depends(get_db)): return templates.TemplateResponse("jobs.html", { diff --git a/app/templates/printer_types.html b/app/templates/printer_types.html new file mode 100644 index 0000000..2620a78 --- /dev/null +++ b/app/templates/printer_types.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} +{% block title %}Printer Types — POPS{% endblock %} + +{% block content %} +
+
+ +
Printer Types
+
+ +
+ +
+ {% if printer_types %} + + + + + + + + + + + + {% for t in printer_types %} + + + + + + + + {% endfor %} + +
NameBed X (mm)Bed Y (mm)Bed Z (mm)Volume
{{ t.name }}{{ t.bed_x_mm }}{{ t.bed_y_mm }}{{ t.bed_z_mm }} + {{ (t.bed_x_mm * t.bed_y_mm * t.bed_z_mm / 1000) | round(0) | int }} cm³ +
+ {% else %} +
No printer types defined yet.
+ {% endif %} +
+ +{# ── Add Type Modal ── #} + +{% endblock %} diff --git a/app/templates/printers.html b/app/templates/printers.html index 79da7b6..c5af27f 100644 --- a/app/templates/printers.html +++ b/app/templates/printers.html @@ -2,24 +2,49 @@ {% block title %}Printers — POPS{% endblock %} {% block content %} -
Printers
+
+
Printers
+
+ + Manage Types + + +
+
+ +{% if not printer_types %} + +{% endif %} +
{% if printers %} - + + - {% for p in printers %} - + + - {% endfor %}
NameModelTypeBed volume Location StatusAdded
{{ p.name }}{{ p.model or '—' }}{{ p.printer_type.name if p.printer_type else '—' }} + {% if p.printer_type %} + {{ p.printer_type.bed_x_mm }}×{{ p.printer_type.bed_y_mm }}×{{ p.printer_type.bed_z_mm }} mm + {% else %}—{% endif %} + {{ p.location or '—' }} {% set s = p.status.value %} @@ -29,13 +54,55 @@ {% else %}offline {% endif %} {{ p.created_at.strftime('%d.%m.%Y') }}
{% else %} -
No printers configured yet.
+
No printers added yet.
{% endif %}
+ +{# ── Add Printer Modal ── #} + {% endblock %}