178 lines
5.5 KiB
Python
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)
|