diff --git a/frontend/src/pages/Calibration.tsx b/frontend/src/pages/Calibration.tsx index acf285b..8a723e7 100644 --- a/frontend/src/pages/Calibration.tsx +++ b/frontend/src/pages/Calibration.tsx @@ -28,6 +28,7 @@ import { Circle, Camera, ShieldQuestion, + Save, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import Logo from "@/components/Logo"; @@ -449,11 +450,13 @@ const Calibration = () => { // re-detected USB port (which shuffles on reboot/reconnect) sticks without // needing a full re-calibration. Mirrors the camera write-back above. const persistPort = useCallback( - async (nextPort: string) => { - if (!robotName || !nextPort) return; + async ( + nextPort: string + ): Promise<"saved" | "unchanged" | "skipped" | "error"> => { + if (!robotName || !nextPort) return "skipped"; const field = deviceType === "robot" ? "follower_port" : "leader_port"; // Skip redundant writes when the value already matches the record. - if (robot && robot[field] === nextPort) return; + if (robot && robot[field] === nextPort) return "unchanged"; try { const res = await fetchWithHeaders( `${baseUrl}/robots/${encodeURIComponent(robotName)}`, @@ -463,10 +466,16 @@ const Calibration = () => { body: JSON.stringify({ [field]: nextPort }), } ); + if (!res.ok) { + console.error("Failed to save port to robot record: HTTP", res.status); + return "error"; + } const data = await res.json(); if (data.robot) setRobot(data.robot); + return "saved"; } catch (e) { console.error("Failed to save port to robot record:", e); + return "error"; } }, [robotName, deviceType, robot, baseUrl, fetchWithHeaders] @@ -477,6 +486,45 @@ const Calibration = () => { persistPort(detectedPort); }; + // Explicit "Save port" affordance. The port also autosaves on blur, but users + // expect a visible button + confirmation (issue #53) — especially when a + // reconnect shuffles the serial port and they want to update it WITHOUT + // redoing the whole calibration. + const handleSavePort = useCallback(async () => { + if (!robotName) { + toast({ + title: "No robot selected", + description: + "Open Calibration from a robot's gear icon on the Landing page.", + variant: "destructive", + }); + return; + } + if (!port) { + toast({ + title: "Missing port", + description: "Enter or detect a serial port first.", + variant: "destructive", + }); + return; + } + const result = await persistPort(port); + const side = deviceType === "robot" ? "follower" : "leader"; + if (result === "saved" || result === "unchanged") { + toast({ + title: "Port saved", + description: `Saved ${port} for the ${side} — no recalibration needed.`, + }); + } else if (result === "error") { + toast({ + title: "Failed to save port", + description: + "Could not save the port to this robot. Check the backend and try again.", + variant: "destructive", + }); + } + }, [robotName, port, deviceType, persistPort, toast]); + const getStatusDisplay = () => { switch (calibrationStatus.status) { case "idle": @@ -612,7 +660,22 @@ const Calibration = () => { robotType={deviceType === "robot" ? "follower" : "leader"} className="border-slate-600 hover:border-blue-500 text-slate-400 hover:text-blue-400 bg-slate-700 hover:bg-slate-600" /> + +

+ Serial ports can change when the arm reconnects. Update the port + and click Save — no need to recalibrate. +