feat: Phase 6, Device provisioning and deployment of updates on git-pull

This commit is contained in:
2026-02-27 04:42:41 +02:00
parent 32a2634739
commit 57259c2c2f
19 changed files with 1670 additions and 26 deletions

View File

@@ -64,6 +64,12 @@ export default function DeviceInventoryDetail() {
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 loadDevice = async () => {
setLoading(true);
setError("");
@@ -99,6 +105,28 @@ export default function DeviceInventoryDetail() {
}
};
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 {
@@ -303,7 +331,7 @@ export default function DeviceInventoryDetail() {
{/* Actions card */}
<div
className="rounded-lg border p-5"
className="rounded-lg border p-5 mb-4"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<h2 className="text-sm font-semibold uppercase tracking-wide mb-3" style={{ color: "var(--text-muted)" }}>
@@ -326,6 +354,77 @@ export default function DeviceInventoryDetail() {
NVS binary encodes device_uid, hw_type, hw_version. Flash at 0x9000.
</p>
</div>
{/* Assign to Customer card */}
{canEdit && ["provisioned", "flashed"].includes(device?.mfg_status) && (
<div
className="rounded-lg border p-5"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<h2 className="text-sm font-semibold uppercase tracking-wide mb-3" style={{ color: "var(--text-muted)" }}>
Assign to Customer
</h2>
{assignSuccess ? (
<div
className="text-sm rounded-md p-3 border"
style={{ backgroundColor: "#0a2e2a", borderColor: "#4dd6c8", color: "#4dd6c8" }}
>
Device assigned and invitation email sent to <strong>{device?.owner}</strong>.
</div>
) : (
<div className="space-y-3">
{assignError && (
<div
className="text-xs rounded p-2 border"
style={{
backgroundColor: "var(--danger-bg)",
borderColor: "var(--danger)",
color: "var(--danger-text)",
}}
>
{assignError}
</div>
)}
<input
type="email"
placeholder="Customer email address"
value={assignEmail}
onChange={(e) => 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)",
}}
/>
<input
type="text"
placeholder="Customer name (optional)"
value={assignName}
onChange={(e) => 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)",
}}
/>
<button
onClick={handleAssign}
disabled={assignSaving || !assignEmail.trim()}
className="px-4 py-1.5 text-sm rounded-md hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
>
{assignSaving ? "Sending…" : "Assign & Send Invite"}
</button>
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
Sets device status to <em>sold</em> and emails the customer their serial number.
</p>
</div>
)}
</div>
)}
</div>
);
}