import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import api from "../api/client";
const BOARD_TYPE_LABELS = {
vesper: "Vesper", vesper_plus: "Vesper+", vesper_pro: "Vesper Pro",
chronos: "Chronos", chronos_pro: "Chronos Pro", agnus_mini: "Agnus Mini", agnus: "Agnus",
};
const STATUS_STYLES = {
manufactured: { bg: "var(--bg-card-hover)", color: "var(--text-muted)" },
flashed: { bg: "var(--badge-blue-bg)", color: "var(--badge-blue-text)" },
provisioned: { bg: "#0a2e2a", color: "#4dd6c8" },
sold: { bg: "#1e1036", color: "#c084fc" },
claimed: { bg: "#2e1a00", color: "#fb923c" },
decommissioned:{ bg: "var(--danger-bg)", color: "var(--danger-text)" },
};
const STATUS_OPTIONS = [
"manufactured", "flashed", "provisioned", "sold", "claimed", "decommissioned",
];
// Statuses that require serial confirmation before delete
const PROTECTED_STATUSES = ["sold", "claimed"];
function formatHwVersion(v) {
if (!v) return "—";
if (/^\d+\.\d+/.test(v)) return `Rev ${v}`;
const n = parseInt(v, 10);
if (!isNaN(n)) return `Rev ${n}.0`;
return `Rev ${v}`;
}
function StatusBadge({ status }) {
const style = STATUS_STYLES[status] || STATUS_STYLES.manufactured;
return (
{status}
);
}
function Field({ label, value, mono = false }) {
return (
);
}
// ─── Delete Confirmation Modal ────────────────────────────────────────────────
function DeleteModal({ device, onConfirm, onCancel, deleting }) {
const isProtected = PROTECTED_STATUSES.includes(device?.mfg_status);
const [typed, setTyped] = useState("");
const confirmed = !isProtected || typed === device?.serial_number;
return (
{isProtected ? "⚠ Delete Protected Device" : "Delete Device"}
{isProtected ? (
<>
This device has status {device.mfg_status} and is linked to a customer.
Deleting it is irreversible and may break the customer's access.
To confirm, type the serial number exactly:
{device.serial_number}
setTyped(e.target.value)}
onPaste={(e) => e.preventDefault()}
placeholder="Type serial number here…"
autoComplete="off"
className="w-full px-3 py-2 rounded-md text-sm border mb-4 font-mono"
style={{
backgroundColor: "var(--bg-input)",
borderColor: typed === device.serial_number ? "var(--accent)" : "var(--border-input)",
color: "var(--text-primary)",
}}
/>
>
) : (
Are you sure you want to delete {device?.serial_number}?
This cannot be undone.
)}
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export default function DeviceInventoryDetail() {
const { sn } = useParams();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const canEdit = hasPermission("manufacturing", "edit");
const canDelete = hasPermission("manufacturing", "delete");
const [device, setDevice] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [editingStatus, setEditingStatus] = useState(false);
const [newStatus, setNewStatus] = useState("");
const [statusNote, setStatusNote] = useState("");
const [statusSaving, setStatusSaving] = useState(false);
const [statusError, setStatusError] = useState("");
const [nvsDownloading, setNvsDownloading] = useState(false);
const [assignEmail, setAssignEmail] = useState("");
const [assignName, setAssignName] = useState("");
const [assignSaving, setAssignSaving] = useState(false);
const [assignError, setAssignError] = useState("");
const [assignSuccess, setAssignSuccess] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleting, setDeleting] = useState(false);
const loadDevice = async () => {
setLoading(true);
setError("");
try {
const data = await api.get(`/manufacturing/devices/${sn}`);
setDevice(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => { loadDevice(); }, [sn]);
const handleStatusSave = async () => {
setStatusError("");
setStatusSaving(true);
try {
const updated = await api.request(`/manufacturing/devices/${sn}/status`, {
method: "PATCH",
body: JSON.stringify({ status: newStatus, note: statusNote || null }),
});
setDevice(updated);
setEditingStatus(false);
setStatusNote("");
} catch (err) {
setStatusError(err.message);
} finally {
setStatusSaving(false);
}
};
const handleAssign = async () => {
setAssignError("");
setAssignSaving(true);
try {
const updated = await api.request(`/manufacturing/devices/${sn}/assign`, {
method: "POST",
body: JSON.stringify({ customer_email: assignEmail, customer_name: assignName || null }),
});
setDevice(updated);
setAssignSuccess(true);
setAssignEmail("");
setAssignName("");
} catch (err) {
setAssignError(err.message);
} finally {
setAssignSaving(false);
}
};
const downloadNvs = async () => {
setNvsDownloading(true);
try {
const token = localStorage.getItem("access_token");
const response = await fetch(`/api/manufacturing/devices/${sn}/nvs.bin`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.detail || "Download failed");
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${sn}_nvs.bin`;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
setError(err.message);
} finally {
setNvsDownloading(false);
}
};
const handleDelete = async () => {
setDeleting(true);
try {
const isProtected = PROTECTED_STATUSES.includes(device?.mfg_status);
const url = `/manufacturing/devices/${sn}${isProtected ? "?force=true" : ""}`;
await api.request(url, { method: "DELETE" });
navigate("/manufacturing");
} catch (err) {
setError(err.message);
setShowDeleteModal(false);
} finally {
setDeleting(false);
}
};
const formatDate = (iso) => {
if (!iso) return "—";
try {
return new Date(iso).toLocaleString("en-US", {
year: "numeric", month: "short", day: "numeric",
hour: "2-digit", minute: "2-digit",
});
} catch { return iso; }
};
if (loading) {
return (
);
}
if (error && !device) {
return (
);
}
return (
{/* Title row */}
{device?.serial_number}
{device?.device_name && (
{device.device_name}
)}
{canDelete && (
)}
{error && (
{error}
)}
{/* Identity card */}
Device Identity
{device?.device_name && (
)}
{/* Status card */}
Status
{canEdit && !editingStatus && (
)}
{!editingStatus ? (
) : (
{statusError && (
{statusError}
)}
setStatusNote(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
/>
)}
{/* Actions card */}
Actions
NVS binary encodes device_uid, hw_type, hw_version. Flash at 0x9000.
{/* Assign to Customer card */}
{canEdit && ["provisioned", "flashed"].includes(device?.mfg_status) && (
Assign to Customer
{assignSuccess ? (
Device assigned and invitation email sent to {device?.owner}.
) : (
{assignError && (
{assignError}
)}
setAssignEmail(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
/>
setAssignName(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
/>
Sets device status to sold and emails the customer their serial number.
)}
)}
{/* Delete modal */}
{showDeleteModal && (
setShowDeleteModal(false)}
deleting={deleting}
/>
)}
);
}