update: overhauled firmware ui. Added public flash page.

This commit is contained in:
2026-03-18 17:49:40 +02:00
parent 4381a6681d
commit d0ac4f1d91
45 changed files with 6798 additions and 1723 deletions

View File

@@ -33,6 +33,18 @@ def _get_existing_sns(db) -> set:
return existing
def _resolve_user_list(raw_list: list) -> list[str]:
"""Convert user_list entries (DocumentReferences or path strings) to plain user ID strings."""
from google.cloud.firestore_v1 import DocumentReference
result = []
for entry in raw_list:
if isinstance(entry, DocumentReference):
result.append(entry.id)
elif isinstance(entry, str):
result.append(entry.split("/")[-1])
return result
def _doc_to_inventory_item(doc) -> DeviceInventoryItem:
data = doc.to_dict() or {}
created_raw = data.get("created_at")
@@ -52,6 +64,9 @@ def _doc_to_inventory_item(doc) -> DeviceInventoryItem:
owner=data.get("owner"),
assigned_to=data.get("assigned_to"),
device_name=data.get("device_name") or None,
lifecycle_history=data.get("lifecycle_history") or [],
customer_id=data.get("customer_id"),
user_list=_resolve_user_list(data.get("user_list") or []),
)
@@ -80,11 +95,19 @@ def create_batch(data: BatchCreate) -> BatchResponse:
"created_at": now,
"owner": None,
"assigned_to": None,
"users_list": [],
"user_list": [],
# Legacy fields left empty so existing device views don't break
"device_name": "",
"device_location": "",
"is_Online": False,
"lifecycle_history": [
{
"status_id": "manufactured",
"date": now.isoformat(),
"note": None,
"set_by": None,
}
],
})
serial_numbers.append(sn)
@@ -135,14 +158,31 @@ def get_device_by_sn(sn: str) -> DeviceInventoryItem:
return _doc_to_inventory_item(docs[0])
def update_device_status(sn: str, data: DeviceStatusUpdate) -> DeviceInventoryItem:
def update_device_status(sn: str, data: DeviceStatusUpdate, set_by: str | None = None) -> DeviceInventoryItem:
db = get_db()
docs = list(db.collection(COLLECTION).where("serial_number", "==", sn).limit(1).stream())
if not docs:
raise NotFoundError("Device")
doc_ref = docs[0].reference
update = {"mfg_status": data.status.value}
doc_data = docs[0].to_dict() or {}
now = datetime.now(timezone.utc).isoformat()
history = doc_data.get("lifecycle_history") or []
# Append new lifecycle entry
new_entry = {
"status_id": data.status.value,
"date": now,
"note": data.note if data.note else None,
"set_by": set_by,
}
history.append(new_entry)
update = {
"mfg_status": data.status.value,
"lifecycle_history": history,
}
if data.note:
update["mfg_status_note"] = data.note
doc_ref.update(update)
@@ -150,47 +190,114 @@ def update_device_status(sn: str, data: DeviceStatusUpdate) -> DeviceInventoryIt
return _doc_to_inventory_item(doc_ref.get())
def get_nvs_binary(sn: str) -> bytes:
def get_nvs_binary(sn: str, hw_type_override: str | None = None, hw_revision_override: str | None = None) -> bytes:
item = get_device_by_sn(sn)
return generate_nvs_binary(
serial_number=item.serial_number,
hw_type=item.hw_type,
hw_version=item.hw_version,
hw_family=hw_type_override if hw_type_override else item.hw_type,
hw_revision=hw_revision_override if hw_revision_override else item.hw_version,
)
def assign_device(sn: str, data: DeviceAssign) -> DeviceInventoryItem:
from utils.email import send_device_assignment_invite
"""Assign a device to a customer by customer_id.
- Stores customer_id on the device doc.
- Adds the device to the customer's owned_items list.
- Sets mfg_status to 'sold' unless device is already 'claimed'.
"""
db = get_db()
CRM_COLLECTION = "crm_customers"
# Get device doc
docs = list(db.collection(COLLECTION).where("serial_number", "==", sn).limit(1).stream())
if not docs:
raise NotFoundError("Device")
doc_data = docs[0].to_dict() or {}
doc_ref = docs[0].reference
doc_ref.update({
"owner": data.customer_email,
"assigned_to": data.customer_email,
"mfg_status": "sold",
current_status = doc_data.get("mfg_status", "manufactured")
# Get customer doc
customer_ref = db.collection(CRM_COLLECTION).document(data.customer_id)
customer_doc = customer_ref.get()
if not customer_doc.exists:
raise NotFoundError("Customer")
customer_data = customer_doc.to_dict() or {}
# Determine new status: don't downgrade claimed → sold
new_status = current_status if current_status == "claimed" else "sold"
now = datetime.now(timezone.utc).isoformat()
history = doc_data.get("lifecycle_history") or []
history.append({
"status_id": new_status,
"date": now,
"note": "Assigned to customer",
"set_by": None,
})
hw_type = doc_data.get("hw_type", "")
device_name = BOARD_TYPE_LABELS.get(hw_type, hw_type or "Device")
doc_ref.update({
"customer_id": data.customer_id,
"mfg_status": new_status,
"lifecycle_history": history,
})
try:
send_device_assignment_invite(
customer_email=data.customer_email,
serial_number=sn,
device_name=device_name,
customer_name=data.customer_name,
)
except Exception as exc:
logger.error("Assignment succeeded but email failed for %s%s: %s", sn, data.customer_email, exc)
# Add to customer's owned_items (avoid duplicates)
owned_items = customer_data.get("owned_items", []) or []
device_doc_id = docs[0].id
already_assigned = any(
item.get("type") == "console_device"
and item.get("console_device", {}).get("device_id") == device_doc_id
for item in owned_items
)
if not already_assigned:
device_name = doc_data.get("device_name") or BOARD_TYPE_LABELS.get(doc_data.get("hw_type", ""), sn)
owned_items.append({
"type": "console_device",
"console_device": {
"device_id": device_doc_id,
"serial_number": sn,
"label": device_name,
},
})
customer_ref.update({"owned_items": owned_items})
return _doc_to_inventory_item(doc_ref.get())
def search_customers(q: str) -> list:
"""Search crm_customers by name, email, phone, organization, or tags."""
db = get_db()
CRM_COLLECTION = "crm_customers"
docs = db.collection(CRM_COLLECTION).stream()
results = []
q_lower = q.lower().strip()
for doc in docs:
data = doc.to_dict() or {}
loc = data.get("location") or {}
loc = loc if isinstance(loc, dict) else {}
city = loc.get("city") or ""
searchable = " ".join(filter(None, [
data.get("name"), data.get("surname"),
data.get("email"), data.get("phone"), data.get("organization"),
loc.get("address"), loc.get("city"), loc.get("postal_code"),
loc.get("region"), loc.get("country"),
" ".join(data.get("tags") or []),
])).lower()
if not q_lower or q_lower in searchable:
results.append({
"id": doc.id,
"name": data.get("name") or "",
"surname": data.get("surname") or "",
"email": data.get("email") or "",
"organization": data.get("organization") or "",
"phone": data.get("phone") or "",
"city": city or "",
})
return results
def get_stats() -> ManufacturingStats:
db = get_db()
docs = list(db.collection(COLLECTION).stream())