mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 15:54:27 +02:00
A group can now be limited to a set of printers. Its members see and control only those printers. Every other printer answers 404, as if it didn't exist. - Groups gain restrict_printers and a group_printers table (migration for SQLite and Postgres). A user's printers are the union of their limited groups. Groups without the flag don't limit anything, a user in no limited group keeps every printer, and admins see all of them. - core/printer_scope.py holds the scope. RequestPrinterScope and RequirePrinterPermissionIfAuthEnabled apply it to routes: printer routes, camera, queue and batches, archives, projects, stats, print log, pipeline runs, inventory and Spoolman assignments, maintenance, smart plugs, scheduled drying, firmware and Obico status. - API keys, camera stream, Cam Wall, overlay and WebSocket tokens carry the printers of whoever created them. WebSocket broadcasts are filtered per connection, and the filtering fails closed. - Scheduler: "Any <model>" jobs stay on their owner's printers. A job pinned to a printer its owner lost waits with a reason. Callers with no user identity and limited printers must queue to a specific printer. - Group editor: new Printer access section, translated into all 15 locales. Saving a system group no longer resends unchanged permissions, which the backend refused.
115 lines
4.8 KiB
Python
115 lines
4.8 KiB
Python
"""Scheduled (delayed) manual AMS drying runs (#2638)."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.core.auth import RequestPrinterScope, RequirePermissionIfAuthEnabled
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.core.printer_scope import PrinterScope
|
|
from backend.app.models.printer import Printer
|
|
from backend.app.models.scheduled_drying import ScheduledDrying
|
|
from backend.app.models.user import User
|
|
from backend.app.schemas.scheduled_drying import ScheduledDryingCreate, ScheduledDryingResponse
|
|
from backend.app.services import drying_preflight
|
|
from backend.app.services.printer_manager import printer_manager
|
|
from backend.app.utils.local_time import utcnow_naive
|
|
|
|
router = APIRouter(prefix="/scheduled-dryings", tags=["scheduled-dryings"])
|
|
|
|
ACTIVE_STATUSES = ("pending", "running")
|
|
# Failed rows are listed too. A run can only fail at dispatch (the firmware
|
|
# turns out too old, say), which is exactly the case a schedule-time check on
|
|
# an offline printer cannot catch, so without this the run just disappears and
|
|
# only the backend log knows why. The client dismisses the row to clear it.
|
|
LISTED_STATUSES = (*ACTIVE_STATUSES, "failed")
|
|
|
|
|
|
@router.post("", response_model=ScheduledDryingResponse)
|
|
async def create_scheduled_drying(
|
|
payload: ScheduledDryingCreate,
|
|
user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
|
|
printer_scope: PrinterScope = RequestPrinterScope,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
printer_scope.ensure(payload.printer_id)
|
|
result = await db.execute(select(Printer).where(Printer.id == payload.printer_id))
|
|
printer = result.scalar_one_or_none()
|
|
if not printer:
|
|
raise HTTPException(404, "Printer not found")
|
|
|
|
# Fail fast in the UI rather than hours later with nobody watching. An
|
|
# offline printer is still schedulable: only its model is judged here.
|
|
state = printer_manager.get_status(payload.printer_id)
|
|
unsupported = drying_preflight.check_drying_supported(
|
|
printer.model,
|
|
state.firmware_version if state else None,
|
|
require_firmware=state is not None,
|
|
)
|
|
if unsupported:
|
|
raise HTTPException(400, unsupported)
|
|
|
|
if payload.start_after is not None and payload.start_after <= utcnow_naive():
|
|
raise HTTPException(400, "start_after must be in the future")
|
|
|
|
row = ScheduledDrying(
|
|
printer_id=payload.printer_id,
|
|
ams_id=payload.ams_id,
|
|
temp=payload.temp,
|
|
duration_hours=payload.duration_hours,
|
|
filament=payload.filament,
|
|
rotate_tray=payload.rotate_tray,
|
|
start_after=payload.start_after,
|
|
created_by_id=user.id if user else None,
|
|
)
|
|
db.add(row)
|
|
await db.commit()
|
|
await db.refresh(row)
|
|
return row
|
|
|
|
|
|
@router.get("", response_model=list[ScheduledDryingResponse])
|
|
async def list_scheduled_dryings(
|
|
printer_id: int | None = None,
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
|
|
printer_scope: PrinterScope = RequestPrinterScope,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
query = select(ScheduledDrying).where(ScheduledDrying.status.in_(LISTED_STATUSES))
|
|
if (clause := printer_scope.where_strict(ScheduledDrying.printer_id)) is not None:
|
|
query = query.where(clause)
|
|
if printer_id is not None:
|
|
query = query.where(ScheduledDrying.printer_id == printer_id)
|
|
result = await db.execute(query.order_by(ScheduledDrying.start_after.asc().nullsfirst(), ScheduledDrying.id.asc()))
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.delete("/{scheduled_drying_id}")
|
|
async def cancel_scheduled_drying(
|
|
scheduled_drying_id: int,
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
|
|
printer_scope: PrinterScope = RequestPrinterScope,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(ScheduledDrying).where(ScheduledDrying.id == scheduled_drying_id))
|
|
row = result.scalar_one_or_none()
|
|
if not row or not printer_scope.allows(row.printer_id):
|
|
raise HTTPException(404, "Scheduled drying not found")
|
|
if row.status == "failed":
|
|
# Terminal and now acknowledged; drop it so it stops being listed.
|
|
await db.delete(row)
|
|
await db.commit()
|
|
return {"status": "dismissed", "id": scheduled_drying_id}
|
|
if row.status not in ACTIVE_STATUSES:
|
|
raise HTTPException(400, "Only pending, running or failed dryings can be cancelled")
|
|
|
|
if row.status == "running":
|
|
# Best effort; cancellation proceeds even if the printer is offline.
|
|
printer_manager.send_drying_command(row.printer_id, row.ams_id, 0, 0, mode=0)
|
|
|
|
row.status = "cancelled"
|
|
row.completed_at = utcnow_naive()
|
|
await db.commit()
|
|
return {"status": "cancelled", "id": row.id}
|