============================================================ LIMBFORGE — 3D MEASUREMENT SAVE AUDIT ============================================================ UTC: 2026-08-05T01:51:22+00:00 ------------------------------------------------------------ 1. LIVE SERVICES ------------------------------------------------------------ shire-limbforge-workspace.service active shire-limbforge-canvas-gateway.service active http://127.0.0.1:8787/health 200 http://127.0.0.1:8786/health 200 http://127.0.0.1:8787/agents/limbforge/measurements 200 ------------------------------------------------------------ 2. LIVE MEASUREMENT RECORD LINKAGE ------------------------------------------------------------ Measurement sessions: 1 Components: 0 Cover projects: 1 Measurement values: 0 LATEST MEASUREMENT SESSIONS 1. uuid=session-02915477-3775-4868-8c89-fad351ca6ab8 status='Not started' workflow='Osseointegration' progress=0 project_uuid='proj-3581fe0a-cfd5-4341-af75-eaab8186137b' link=VALID component_uuid=None link=MISSING saved_values=0 FAULT_CANDIDATE: SESSION_HAS_NO_COMPONENT_UUID ------------------------------------------------------------ 3. SAVE-PATH SOURCE CONDITIONS ------------------------------------------------------------ === src/components/workshop3d/ContextInspectorV2.jsx === 1 | import { useEffect, useState } from 'react'; 2 | import { Ruler, Box, Layers, Footprints, ShieldAlert, Save, RotateCcw, Ban, History, CheckCircle2, AlertTriangle, XCircle } from 'lucide-react'; 3 | import { base44 } from '@/api/base44Client'; >> 4 | import { saveStationMeasurement, GEOMETRY_STATUS, STATUS_TONE } from '@/services/measurementGeometryService'; 5 | 6 | const STATION_LABELS = { 7 | 'pylon': 'Pylon Diameter', 8 | 'cover-lower': 'Cover Lower Circumference', 9 | 'socket-distal': 'Socket Distal Circumference', 10 | 'socket-middle': 'Socket Middle Circumference', 11 | 'cover-widest': 'Widest Cover / Calf Region', 12 | 'socket-upper': 'Socket Upper Circumference', 13 | 'socket-proximal': 'Socket Proximal Boundary', 24 | 25 | function Pill({ tone, children }) { 26 | const map = { green: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300', amber: 'border-amber-400/40 bg-amber-400/10 text-amber-300', red: 'border-red-400/40 bg-red-400/10 text-red-300', purple: 'border-violet-400/40 bg-violet-400/10 text-violet-300', slate: 'border-white/15 bg-white/5 text-slate-300' }; 27 | return {children}; 28 | } 29 | >> 30 | export default function ContextInspectorV2({ selection, project, sessionId, componentUuid, geometry, onSaved }) { 31 | const [busy, setBusy] = useState(false); 32 | const [form, setForm] = useState({ total_width_mm: '', total_depth_mm: '', circumference_mm: '', height_from_origin_mm: '', notes: '' }); 33 | const [existing, setExisting] = useState(null); 34 | const [linkedComponent, setLinkedComponent] = useState(null); 35 | 36 | const kind = selection ? selection.type : null; 37 | const isStation = kind === 'measurementStation'; 38 | const isPart = kind === 'prosthesisPart'; 39 | const isCover = kind === 'cover'; 40 | const partId = isPart ? selection.partId : isCover ? 'cover-shell' : null; 41 | 42 | // Load existing station value when a station is selected 43 | useEffect(() => { >> 44 | if (!isStation || !sessionId) return; 45 | let alive = true; 46 | const name = selection.stationName || STATION_LABELS[selection.stationId] || selection.stationId; 47 | base44.entities.EnvelopeStation.filter({ session_uuid: sessionId, station_name: name }) 48 | .then((list) => { 49 | if (!alive) return; 50 | const rec = list && list[0]; 51 | setExisting(rec || null); 52 | if (rec) { 53 | setForm({ 54 | total_width_mm: rec.total_width_mm ?? '', 55 | total_depth_mm: rec.total_depth_mm ?? '', 56 | circumference_mm: rec.circumference_mm ?? '', 60 | } else { 61 | setForm({ total_width_mm: '', total_depth_mm: '', circumference_mm: '', height_from_origin_mm: '', notes: '' }); 62 | } 63 | }) 64 | .catch(() => { if (alive) setExisting(null); }); 65 | return () => { alive = false; }; >> 66 | }, [isStation, sessionId, selection && selection.stationId]); 67 | 68 | // Load linked component for hardware parts 69 | useEffect(() => { 70 | if (!isPart || !project) return; 71 | const uuids = project.component_uuids || []; 72 | if (!uuids.length) { setLinkedComponent(null); return; } 73 | let alive = true; 74 | base44.entities.Component.filter({ uuid: { $in: uuids } }) 75 | .then((list) => { if (alive) setLinkedComponent((list || [])[0] || null); }) 76 | .catch(() => { if (alive) setLinkedComponent(null); }); 77 | return () => { alive = false; }; 78 | }, [isPart, project && project.uuid]); 79 | 80 | const handleSave = async () => { >> 81 | if (!isStation || !sessionId) return; 82 | setBusy(true); 83 | try { 84 | const name = selection.stationName || STATION_LABELS[selection.stationId] || selection.stationId; 85 | await saveStationMeasurement({ 86 | sessionUuid: sessionId, 87 | componentUuid: componentUuid || (project && (project.component_uuids || [])[0]) || 'generic', 88 | station: { 89 | station_name: name, 90 | total_width_mm: form.total_width_mm, 91 | total_depth_mm: form.total_depth_mm, 92 | circumference_mm: form.circumference_mm, 93 | height_from_origin_mm: form.height_from_origin_mm, 94 | notes: form.notes, 95 | }, 96 | }); 97 | onSaved && onSaved(); 98 | } finally { setBusy(false); } 99 | }; 100 | 101 | const handleNotApplicable = async () => { >> 102 | if (!isStation || !sessionId) return; 103 | setBusy(true); 104 | try { 105 | const name = selection.stationName || STATION_LABELS[selection.stationId] || selection.stationId; 106 | await saveStationMeasurement({ sessionUuid: sessionId, componentUuid: componentUuid || 'generic', station: { station_name: name, notes: 'Not applicable', shape_classification: 'Not applicable' } }); 107 | onSaved && onSaved(); 108 | } finally { setBusy(false); } 109 | }; 110 | 111 | const statusTone = geometry ? STATUS_TONE[geometry.geometryStatus] : 'slate'; 112 | 113 | return ( 114 |
115 |
139 | setForm((f) => ({ ...f, total_depth_mm: v }))} /> 140 | setForm((f) => ({ ...f, circumference_mm: v }))} /> 141 | setForm((f) => ({ ...f, height_from_origin_mm: v }))} /> 142 |
143 |