The inventory list and detail page previously only showed raw customer_id / user_list references. list_devices now batch-resolves customer names and user display names/emails (via Firestore get_all(), avoiding an N+1 round trip per device) and exposes them as customer_name / users on DeviceInventoryItem. Search now matches against device name, console name, customer name, and assigned users in addition to serial/owner/batch. Also adds hw_types (multi-select board type) and has_users filters to GET /manufacturing/devices, and carries console_name through for display. DeviceInventoryDetail is updated to show and use all of this (customer name, assigned users, new UI components for signal/charts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
3.5 KiB
Python
146 lines
3.5 KiB
Python
from pydantic import BaseModel, Field
|
|
from typing import Optional, List
|
|
from enum import Enum
|
|
|
|
|
|
class BoardType(str, Enum):
|
|
vesper = "vesper"
|
|
vesper_plus = "vesper_plus"
|
|
vesper_pro = "vesper_pro"
|
|
chronos = "chronos"
|
|
chronos_pro = "chronos_pro"
|
|
agnus_mini = "agnus_mini"
|
|
agnus = "agnus"
|
|
|
|
|
|
BOARD_TYPE_LABELS = {
|
|
"vesper": "Vesper",
|
|
"vesper_plus": "Vesper Plus",
|
|
"vesper_pro": "Vesper Pro",
|
|
"chronos": "Chronos",
|
|
"chronos_pro": "Chronos Pro",
|
|
"agnus_mini": "Agnus Mini",
|
|
"agnus": "Agnus",
|
|
}
|
|
|
|
# Family codes (BS + 4 chars = segment 1 of serial number)
|
|
BOARD_FAMILY_CODES = {
|
|
"vesper": "VSPR",
|
|
"vesper_plus": "VSPR",
|
|
"vesper_pro": "VSPR",
|
|
"agnus": "AGNS",
|
|
"agnus_mini": "AGNS",
|
|
"chronos": "CRNS",
|
|
"chronos_pro": "CRNS",
|
|
}
|
|
|
|
# Variant codes (3 chars = first part of segment 3 of serial number)
|
|
BOARD_VARIANT_CODES = {
|
|
"vesper": "STD",
|
|
"vesper_plus": "PLS",
|
|
"vesper_pro": "PRO",
|
|
"agnus": "STD",
|
|
"agnus_mini": "MIN",
|
|
"chronos": "STD",
|
|
"chronos_pro": "PRO",
|
|
}
|
|
|
|
|
|
class MfgStatus(str, Enum):
|
|
manufactured = "manufactured"
|
|
flashed = "flashed"
|
|
provisioned = "provisioned"
|
|
sold = "sold"
|
|
claimed = "claimed"
|
|
decommissioned = "decommissioned"
|
|
|
|
|
|
class LifecycleEntry(BaseModel):
|
|
status_id: str
|
|
date: str # ISO 8601 UTC string
|
|
note: Optional[str] = None
|
|
set_by: Optional[str] = None
|
|
|
|
|
|
class BatchCreate(BaseModel):
|
|
board_type: BoardType
|
|
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)
|
|
|
|
|
|
class BatchResponse(BaseModel):
|
|
batch_id: str
|
|
serial_numbers: List[str]
|
|
board_type: str
|
|
board_version: str
|
|
created_at: str
|
|
|
|
|
|
class DeviceUserSummary(BaseModel):
|
|
user_id: str
|
|
display_name: str = ""
|
|
email: str = ""
|
|
|
|
|
|
class DeviceInventoryItem(BaseModel):
|
|
id: str
|
|
serial_number: str
|
|
hw_type: str
|
|
hw_version: str
|
|
mfg_status: str
|
|
mfg_batch_id: Optional[str] = None
|
|
created_at: Optional[str] = None
|
|
owner: Optional[str] = None
|
|
assigned_to: Optional[str] = None
|
|
device_name: Optional[str] = None
|
|
# Console-only friendly name (mirrors devices.models.DeviceCreate.console_name).
|
|
# Only populated once a device has been claimed/onboarded in the app — null
|
|
# for freshly manufactured inventory, which is expected and should render
|
|
# as a graceful fallback (e.g. serial number) on the frontend.
|
|
console_name: Optional[str] = None
|
|
lifecycle_history: Optional[List["LifecycleEntry"]] = None
|
|
customer_id: Optional[str] = None
|
|
customer_name: Optional[str] = None
|
|
user_list: Optional[List[str]] = None
|
|
users: Optional[List[DeviceUserSummary]] = None
|
|
|
|
|
|
class DeviceInventoryListResponse(BaseModel):
|
|
devices: List[DeviceInventoryItem]
|
|
total: int
|
|
|
|
|
|
class DeviceStatusUpdate(BaseModel):
|
|
status: MfgStatus
|
|
note: Optional[str] = None
|
|
force_claimed: bool = False
|
|
|
|
|
|
class DeviceAssign(BaseModel):
|
|
customer_id: str
|
|
|
|
|
|
class CustomerSearchResult(BaseModel):
|
|
id: str
|
|
name: str = ""
|
|
email: str = ""
|
|
organization: str = ""
|
|
phone: str = ""
|
|
|
|
|
|
class RecentActivityItem(BaseModel):
|
|
serial_number: str
|
|
hw_type: str
|
|
mfg_status: str
|
|
owner: Optional[str] = None
|
|
updated_at: Optional[str] = None
|
|
|
|
|
|
class ManufacturingStats(BaseModel):
|
|
counts: dict
|
|
recent_activity: List[RecentActivityItem]
|