style: Updated the overall UI of the provisining pages

This commit is contained in:
2026-02-27 12:23:17 +02:00
parent 47570257bd
commit 7585e43b52
8 changed files with 1922 additions and 848 deletions

View File

@@ -5,14 +5,22 @@ from enum import Enum
class BoardType(str, Enum):
vs = "vs" # Vesper
vp = "vp" # Vesper+
vx = "vx" # VesperPro
vp = "vp" # Vesper Plus
vx = "vx" # Vesper Pro
cb = "cb" # Chronos
cp = "cp" # Chronos Pro
am = "am" # Agnus Mini
ab = "ab" # Agnus
BOARD_TYPE_LABELS = {
"vs": "Vesper",
"vp": "Vesper+",
"vx": "VesperPro",
"vp": "Vesper Plus",
"vx": "Vesper Pro",
"cb": "Chronos",
"cp": "Chronos Pro",
"am": "Agnus Mini",
"ab": "Agnus",
}
@@ -27,7 +35,11 @@ class MfgStatus(str, Enum):
class BatchCreate(BaseModel):
board_type: BoardType
board_version: str = Field(..., pattern=r"^\d{2}$", description="2-digit zero-padded version, e.g. '01'")
board_version: str = Field(
...,
pattern=r"^\d+(\.\d+)*$",
description="SemVer-style version string, e.g. '1.0' or legacy '01'",
)
quantity: int = Field(..., ge=1, le=100)
@@ -49,6 +61,7 @@ class DeviceInventoryItem(BaseModel):
created_at: Optional[str] = None
owner: Optional[str] = None
assigned_to: Optional[str] = None
device_name: Optional[str] = None
class DeviceInventoryListResponse(BaseModel):

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi.responses import Response
from fastapi.responses import RedirectResponse
from typing import Optional
@@ -13,6 +13,7 @@ from manufacturing.models import (
)
from manufacturing import service
from manufacturing import audit
from shared.exceptions import NotFoundError
router = APIRouter(prefix="/api/manufacturing", tags=["manufacturing"])
@@ -129,6 +130,41 @@ async def assign_device(
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,

View File

@@ -46,6 +46,7 @@ def _doc_to_inventory_item(doc) -> DeviceInventoryItem:
created_at=created_str,
owner=data.get("owner"),
assigned_to=data.get("assigned_to"),
device_name=data.get("device_name") or None,
)
@@ -217,6 +218,47 @@ def get_stats() -> ManufacturingStats:
return ManufacturingStats(counts=counts, recent_activity=recent)
PROTECTED_STATUSES = {"sold", "claimed"}
def delete_device(sn: str, force: bool = False) -> None:
"""Delete a device by serial number.
Raises PermissionError if the device is sold/claimed and force is not set.
The frontend uses force=True only after the user confirms by typing the SN.
"""
db = get_db()
docs = list(db.collection(COLLECTION).where("serial_number", "==", sn).limit(1).stream())
if not docs:
raise NotFoundError("Device")
data = docs[0].to_dict() or {}
status = data.get("mfg_status", "manufactured")
if status in PROTECTED_STATUSES and not force:
raise PermissionError(
f"Device {sn} has status '{status}' and cannot be deleted without explicit confirmation."
)
docs[0].reference.delete()
def delete_unprovisioned_devices() -> list[str]:
"""Delete all devices with status 'manufactured' (never flashed/provisioned).
Returns the list of deleted serial numbers.
"""
db = get_db()
docs = list(db.collection(COLLECTION).where("mfg_status", "==", "manufactured").stream())
deleted = []
for doc in docs:
data = doc.to_dict() or {}
sn = data.get("serial_number", "")
doc.reference.delete()
deleted.append(sn)
return deleted
def get_firmware_url(sn: str) -> str:
"""Return the FastAPI download URL for the latest stable firmware for this device's hw_type."""
from firmware.service import get_latest