update: added assets manager and extra nvs settings on cloudflash
This commit is contained in:
@@ -201,13 +201,18 @@ async def patch_lifecycle_entry(
|
||||
return _doc_to_inventory_item(doc_ref.get())
|
||||
|
||||
|
||||
@router.post("/devices/{sn}/lifecycle", response_model=DeviceInventoryItem, status_code=201)
|
||||
@router.post("/devices/{sn}/lifecycle", response_model=DeviceInventoryItem, status_code=200)
|
||||
async def create_lifecycle_entry(
|
||||
sn: str,
|
||||
body: LifecycleEntryCreate,
|
||||
user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
|
||||
):
|
||||
"""Create a lifecycle history entry for a step that has no entry yet (on-the-fly)."""
|
||||
"""Upsert a lifecycle history entry for the given status_id.
|
||||
|
||||
If an entry for this status already exists it is overwritten in-place;
|
||||
otherwise a new entry is appended. This prevents duplicate entries when
|
||||
a status is visited more than once (max one entry per status).
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
db = get_firestore()
|
||||
docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
|
||||
@@ -215,14 +220,25 @@ async def create_lifecycle_entry(
|
||||
raise HTTPException(status_code=404, detail="Device not found")
|
||||
doc_ref = docs[0].reference
|
||||
data = docs[0].to_dict() or {}
|
||||
history = data.get("lifecycle_history") or []
|
||||
history = list(data.get("lifecycle_history") or [])
|
||||
|
||||
new_entry = {
|
||||
"status_id": body.status_id,
|
||||
"date": body.date or datetime.now(timezone.utc).isoformat(),
|
||||
"note": body.note,
|
||||
"set_by": user.email,
|
||||
}
|
||||
history.append(new_entry)
|
||||
|
||||
# Overwrite existing entry for this status if present, else append
|
||||
existing_idx = next(
|
||||
(i for i, e in enumerate(history) if e.get("status_id") == body.status_id),
|
||||
None,
|
||||
)
|
||||
if existing_idx is not None:
|
||||
history[existing_idx] = new_entry
|
||||
else:
|
||||
history.append(new_entry)
|
||||
|
||||
doc_ref.update({"lifecycle_history": history})
|
||||
from manufacturing.service import _doc_to_inventory_item
|
||||
return _doc_to_inventory_item(doc_ref.get())
|
||||
@@ -313,6 +329,91 @@ async def delete_device(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/devices/{sn}/email/manufactured", status_code=204)
|
||||
async def send_manufactured_email(
|
||||
sn: str,
|
||||
user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
|
||||
):
|
||||
"""Send the 'device manufactured' notification to the assigned customer's email."""
|
||||
db = get_firestore()
|
||||
docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
|
||||
if not docs:
|
||||
raise HTTPException(status_code=404, detail="Device not found")
|
||||
data = docs[0].to_dict() or {}
|
||||
customer_id = data.get("customer_id")
|
||||
if not customer_id:
|
||||
raise HTTPException(status_code=400, detail="No customer assigned to this device")
|
||||
customer_doc = db.collection("crm_customers").document(customer_id).get()
|
||||
if not customer_doc.exists:
|
||||
raise HTTPException(status_code=404, detail="Assigned customer not found")
|
||||
cdata = customer_doc.to_dict() or {}
|
||||
email = cdata.get("email")
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="Customer has no email address")
|
||||
name_parts = [cdata.get("name") or "", cdata.get("surname") or ""]
|
||||
customer_name = " ".join(p for p in name_parts if p).strip() or None
|
||||
hw_family = data.get("hw_family") or data.get("hw_type") or ""
|
||||
from utils.emails.device_mfged_mail import send_device_manufactured_email
|
||||
send_device_manufactured_email(
|
||||
customer_email=email,
|
||||
serial_number=sn,
|
||||
device_name=hw_family.replace("_", " ").title(),
|
||||
customer_name=customer_name,
|
||||
)
|
||||
await audit.log_action(
|
||||
admin_user=user.email,
|
||||
action="email_manufactured_sent",
|
||||
serial_number=sn,
|
||||
detail={"recipient": email},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/devices/{sn}/email/assigned", status_code=204)
|
||||
async def send_assigned_email(
|
||||
sn: str,
|
||||
user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
|
||||
):
|
||||
"""Send the 'device assigned / app instructions' email to the assigned user(s)."""
|
||||
db = get_firestore()
|
||||
docs = list(db.collection("devices").where("serial_number", "==", sn).limit(1).stream())
|
||||
if not docs:
|
||||
raise HTTPException(status_code=404, detail="Device not found")
|
||||
data = docs[0].to_dict() or {}
|
||||
user_list = data.get("user_list") or []
|
||||
if not user_list:
|
||||
raise HTTPException(status_code=400, detail="No users assigned to this device")
|
||||
hw_family = data.get("hw_family") or data.get("hw_type") or ""
|
||||
device_name = hw_family.replace("_", " ").title()
|
||||
from utils.emails.device_assigned_mail import send_device_assigned_email
|
||||
errors = []
|
||||
for uid in user_list:
|
||||
try:
|
||||
user_doc = db.collection("users").document(uid).get()
|
||||
if not user_doc.exists:
|
||||
continue
|
||||
udata = user_doc.to_dict() or {}
|
||||
email = udata.get("email")
|
||||
if not email:
|
||||
continue
|
||||
display_name = udata.get("display_name") or udata.get("name") or None
|
||||
send_device_assigned_email(
|
||||
user_email=email,
|
||||
serial_number=sn,
|
||||
device_name=device_name,
|
||||
user_name=display_name,
|
||||
)
|
||||
except Exception as exc:
|
||||
errors.append(str(exc))
|
||||
if errors:
|
||||
raise HTTPException(status_code=500, detail=f"Some emails failed: {'; '.join(errors)}")
|
||||
await audit.log_action(
|
||||
admin_user=user.email,
|
||||
action="email_assigned_sent",
|
||||
serial_number=sn,
|
||||
detail={"user_count": len(user_list)},
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/devices", status_code=200)
|
||||
async def delete_unprovisioned(
|
||||
user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
|
||||
@@ -347,6 +448,56 @@ def redirect_firmware(
|
||||
# Upload once per hw_type after each PlatformIO build that changes the layout.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/flash-assets")
|
||||
def list_flash_assets(
|
||||
_user: TokenPayload = Depends(require_permission("manufacturing", "view")),
|
||||
):
|
||||
"""Return asset status for all known board types (and any discovered bespoke UIDs).
|
||||
|
||||
Checks the filesystem directly — no database involved.
|
||||
Each entry contains: hw_type, bootloader (exists, size, uploaded_at), partitions (same), note.
|
||||
"""
|
||||
return {"assets": service.list_flash_assets()}
|
||||
|
||||
|
||||
@router.delete("/flash-assets/{hw_type}/{asset}", status_code=204)
|
||||
async def delete_flash_asset(
|
||||
hw_type: str,
|
||||
asset: str,
|
||||
user: TokenPayload = Depends(require_permission("manufacturing", "delete")),
|
||||
):
|
||||
"""Delete a single flash asset file (bootloader.bin or partitions.bin)."""
|
||||
if asset not in VALID_FLASH_ASSETS:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid asset. Must be one of: {', '.join(sorted(VALID_FLASH_ASSETS))}")
|
||||
try:
|
||||
service.delete_flash_asset(hw_type, asset)
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
await audit.log_action(
|
||||
admin_user=user.email,
|
||||
action="flash_asset_deleted",
|
||||
detail={"hw_type": hw_type, "asset": asset},
|
||||
)
|
||||
|
||||
|
||||
class FlashAssetNoteBody(BaseModel):
|
||||
note: str
|
||||
|
||||
|
||||
@router.put("/flash-assets/{hw_type}/note", status_code=204)
|
||||
async def set_flash_asset_note(
|
||||
hw_type: str,
|
||||
body: FlashAssetNoteBody,
|
||||
_user: TokenPayload = Depends(require_permission("manufacturing", "edit")),
|
||||
):
|
||||
"""Save (or overwrite) the note for a hw_type's flash asset set.
|
||||
|
||||
The note is stored as note.txt next to the binary files.
|
||||
Pass an empty string to clear the note.
|
||||
"""
|
||||
service.set_flash_asset_note(hw_type, body.note)
|
||||
|
||||
|
||||
@router.post("/flash-assets/{hw_type}/{asset}", status_code=204)
|
||||
async def upload_flash_asset(
|
||||
hw_type: str,
|
||||
|
||||
Reference in New Issue
Block a user