Files
bellsystems-cp/backend/manufacturing/router.py

178 lines
5.5 KiB
Python

from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi.responses import Response
from fastapi.responses import RedirectResponse
from typing import Optional
from auth.models import TokenPayload
from auth.dependencies import require_permission
from manufacturing.models import (
BatchCreate, BatchResponse,
DeviceInventoryItem, DeviceInventoryListResponse,
DeviceStatusUpdate, DeviceAssign,
ManufacturingStats,
)
from manufacturing import service
from manufacturing import audit
from shared.exceptions import NotFoundError
router = APIRouter(prefix="/api/manufacturing", tags=["manufacturing"])
@router.get("/stats", response_model=ManufacturingStats)
def get_stats(
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
return service.get_stats()
@router.get("/audit-log")
async def get_audit_log(
limit: int = Query(20, ge=1, le=100),
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
entries = await audit.get_recent(limit=limit)
return {"entries": entries}
@router.post("/batch", response_model=BatchResponse, status_code=201)
async def create_batch(
body: BatchCreate,
user: TokenPayload = Depends(require_permission("manufacturing", "add")),
):
result = service.create_batch(body)
await audit.log_action(
admin_user=user.email,
action="batch_created",
detail={
"batch_id": result.batch_id,
"board_type": result.board_type,
"board_version": result.board_version,
"quantity": len(result.serial_numbers),
},
)
return result
@router.get("/devices", response_model=DeviceInventoryListResponse)
def list_devices(
status: Optional[str] = Query(None),
hw_type: Optional[str] = Query(None),
search: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
items = service.list_devices(
status=status,
hw_type=hw_type,
search=search,
limit=limit,
offset=offset,
)
return DeviceInventoryListResponse(devices=items, total=len(items))
@router.get("/devices/{sn}", response_model=DeviceInventoryItem)
def get_device(
sn: str,
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
return service.get_device_by_sn(sn)
@router.patch("/devices/{sn}/status", response_model=DeviceInventoryItem)
async def update_status(
sn: str,
body: DeviceStatusUpdate,
user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
result = service.update_device_status(sn, body)
await audit.log_action(
admin_user=user.email,
action="status_updated",
serial_number=sn,
detail={"status": body.status.value, "note": body.note},
)
return result
@router.get("/devices/{sn}/nvs.bin")
async def download_nvs(
sn: str,
user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
binary = service.get_nvs_binary(sn)
await audit.log_action(
admin_user=user.email,
action="device_flashed",
serial_number=sn,
)
return Response(
content=binary,
media_type="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{sn}_nvs.bin"'},
)
@router.post("/devices/{sn}/assign", response_model=DeviceInventoryItem)
async def assign_device(
sn: str,
body: DeviceAssign,
user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
):
result = service.assign_device(sn, body)
await audit.log_action(
admin_user=user.email,
action="device_assigned",
serial_number=sn,
detail={"customer_email": body.customer_email, "customer_name": body.customer_name},
)
return result
@router.delete("/devices/{sn}", status_code=204)
async def delete_device(
sn: str,
force: bool = Query(False, description="Required to delete sold/claimed devices"),
user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
"""Delete a device. Sold/claimed devices require force=true."""
try:
service.delete_device(sn, force=force)
except NotFoundError:
raise HTTPException(status_code=404, detail="Device not found")
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
await audit.log_action(
admin_user=user.email,
action="device_deleted",
serial_number=sn,
detail={"force": force},
)
@router.delete("/devices", status_code=200)
async def delete_unprovisioned(
user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
):
"""Delete all devices with status 'manufactured' (never provisioned)."""
deleted = service.delete_unprovisioned_devices()
await audit.log_action(
admin_user=user.email,
action="bulk_delete_unprovisioned",
detail={"count": len(deleted), "serial_numbers": deleted},
)
return {"deleted": deleted, "count": len(deleted)}
@router.get("/devices/{sn}/firmware.bin")
def redirect_firmware(
sn: str,
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
):
"""Redirect to the latest stable firmware binary for this device's hw_type.
Resolves to GET /api/firmware/{hw_type}/stable/{version}/firmware.bin.
"""
url = service.get_firmware_url(sn)
return RedirectResponse(url=url, status_code=302)