import * as THREE from 'three';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import './style.css';

const app = document.querySelector('#app');
app.innerHTML = `
<div class="shell">
  <header class="topbar">
    <div class="brand"><div class="logo">FF</div><div><h1>FlexiForge × SHiRE</h1><p>Approval-gated articulation engineering for SHIRE3D</p></div></div>
    <div class="status-pill"><span class="status-dot"></span><span id="serverStatus">Geometry engine ready</span></div>
  </header>
  <main class="workspace">
    <aside class="panel">
      <section class="section">
        <div class="eyebrow">01 · Source model</div><h2>Import STL</h2>
        <label class="dropzone" id="dropzone"><div class="drop-icon">⬡</div><strong>Drop an STL here</strong><span>or click to browse · max 150 MB</span><input id="fileInput" class="file-input" type="file" accept=".stl" /></label>
      </section>
      <section class="section">
        <div class="eyebrow">Model health</div><h2>Automatic inspection</h2>
        <div class="stat-grid">
          <div class="stat"><label>Triangles</label><strong id="faces">—</strong></div>
          <div class="stat"><label>Watertight</label><strong id="watertight">—</strong></div>
          <div class="stat"><label>Size X</label><strong id="sizeX">—</strong></div>
          <div class="stat"><label>Size Y</label><strong id="sizeY">—</strong></div>
          <div class="stat"><label>Size Z</label><strong id="sizeZ">—</strong></div>
          <div class="stat"><label>Volume</label><strong id="volume">—</strong></div>
        </div>
        <p class="help" id="repairNotes">Upload a model to begin automatic repair and inspection.</p>
      </section>
      <section class="section">
        <div class="eyebrow">02 · Mark movement</div><h2>Joint tool</h2>
        <div class="tool-grid"><button class="tool active" id="selectTool">＋ Click joint</button><button class="tool" id="orbitTool">◎ Orbit model</button></div>
        <p class="help">Click the model where it should bend. FlexiForge estimates the cut direction and local joint size.</p>
        <div id="markerList"></div>
      </section>
      <section class="section shire-section">
        <div class="eyebrow">SHiRE guided mode</div><h2>Ask SHiRE to engineer it</h2>
        <div class="control"><label><span>Project name</span></label><input id="shireProject" type="text" value="SHiRE FlexiForge Project" /></div>
        <div class="control"><label><span>What should move?</span></label><textarea id="shireIntent" rows="3">Turn this STL into an articulated fidget and recommend the safest joint location.</textarea></div>
        <button class="btn shire-wide" id="askShireBtn" disabled>ASK SHiRE FOR JOINTS</button>
        <p class="help">SHiRE may analyse and recommend. It cannot forge or export without your separate approvals.</p>
        <div id="shireSuggestions"></div>
      </section>
    </aside>
    <section class="viewer" id="viewer">
      <div class="viewer-hud"><div class="hud-chip" id="modeChip">Mode: Place joint</div><div class="hud-chip">Left drag: orbit · Wheel: zoom</div></div>
      <div class="empty-state" id="emptyState"><div class="empty-card"><div class="giant">🐉</div><h3>Drop in any STL</h3><p>Then click the body, tail, neck or limb where you want movement. The MVP will engineer a printable assembly joint and export the parts.</p></div></div>
    </section>
    <aside class="panel right">
      <section class="section">
        <div class="eyebrow">03 · Joint intent</div><h2>Movement target</h2>
        <div class="control"><label><span>Movement</span></label><select id="movement"><option value="auto">Let FlexiForge decide</option><option value="wiggle">Wiggle / fidget</option><option value="bend">Single-axis bend</option><option value="rotate">Rotate</option><option value="ball">Full ball movement</option></select></div>
        <div class="control"><label><span>Clearance</span><span id="clearanceValue">0.35 mm</span></label><input id="clearance" type="range" min="0.15" max="0.80" step="0.05" value="0.35" /></div>
        <div class="control"><label><span>Output name</span></label><input id="outputName" type="text" value="flexiforge_fidget" /></div>
      </section>
      <section class="section">
        <div class="eyebrow">04 · Engineering choices</div><h2>Recommended joints</h2>
        <div id="candidates"><div class="notice">Place a joint marker to generate ranked engineering options.</div></div>
      </section>
      <section class="section shire-gate">
        <div class="eyebrow">SHiRE approval gates</div><h2>Controlled production</h2>
        <div class="shire-status"><span>Job</span><strong id="shireJobId">Not started</strong></div>
        <div class="shire-status"><span>State</span><strong id="shireState">—</strong></div>
        <div id="shireGateMessage" class="notice">Ask SHiRE for suggestions, then select one joint design.</div>
        <div class="shire-actions">
          <button class="btn" id="requestDesignBtn" disabled>1. Request design approval</button>
          <code id="designToken" class="approval-token">No design token</code>
          <button class="btn approve" id="approveDesignBtn" disabled>2. RAY APPROVES DESIGN</button>
          <button class="btn" id="shireForgeBtn" disabled>3. SHiRE FORGE</button>
          <button class="btn" id="shireValidateBtn" disabled>4. VALIDATE OUTPUT</button>
          <button class="btn" id="requestExportBtn" disabled>5. Request export approval</button>
          <code id="exportToken" class="approval-token">No export token</code>
          <button class="btn approve" id="approveExportBtn" disabled>6. RAY APPROVES EXPORT</button>
          <button class="btn vault" id="vaultExportBtn" disabled>7. EXPORT TO SHiREVAULT</button>
        </div>
        <p class="help" id="vaultResult">Final export is locked to a verified SHiREVault network mount.</p>
      </section>
      <section class="section">
        <div class="eyebrow">MVP safety</div><div class="notice">This build creates real boolean geometry. Hinge/Flexi exports include a separate pin; ball joints are snap assemblies. Always inspect in your slicer and print a clearance test first.</div>
      </section>
      <section class="section" id="downloadSection" style="display:none"><div class="download-card"><strong>Forge complete</strong><p class="help" id="downloadSummary"></p><a id="downloadLink" href="#">Download printable package →</a></div></section>
    </aside>
  </main>
  <footer class="bottom">
    <button class="btn" id="fitBtn" disabled>Fit view</button><button class="btn danger" id="clearBtn" disabled>Clear joint</button><button class="btn" id="analyseBtn" disabled>Analyse joint</button><button class="btn primary" id="forgeBtn" disabled>STANDALONE FORGE</button>
  </footer>
</div>
<div class="progress" id="progress"><div class="progress-card"><div class="spinner"></div><h2 id="progressTitle">Forging geometry</h2><p class="help" id="progressText">Running mesh repair, slicing and boolean joint construction…</p></div></div>
<div class="toast" id="toast"></div>`;

const state = { modelId: null, modelName: null, modelMesh: null, marker: null, markerObject: null, candidates: [], selectedType: 'auto', mode: 'joint', shireJob: null, designToken: null, exportToken: null };
const viewer = document.querySelector('#viewer');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x090e17);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100000);
camera.position.set(120, 90, 120);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
viewer.prepend(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.07;
controls.screenSpacePanning = true;
scene.add(new THREE.HemisphereLight(0xdde9ff, 0x162033, 2.2));
const key = new THREE.DirectionalLight(0xffffff, 3.4); key.position.set(80, 100, 60); scene.add(key);
const rim = new THREE.DirectionalLight(0xff8b3d, 2.0); rim.position.set(-80, 30, -70); scene.add(rim);
const grid = new THREE.GridHelper(500, 50, 0x30425d, 0x1b2738); grid.rotation.x = Math.PI / 2; grid.position.z = -0.01; scene.add(grid);
const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2();

function resize(){ const r=viewer.getBoundingClientRect(); renderer.setSize(r.width,r.height,false); camera.aspect=r.width/r.height; camera.updateProjectionMatrix(); }
window.addEventListener('resize',resize); resize();
function loop(){ requestAnimationFrame(loop); controls.update(); renderer.render(scene,camera); } loop();

function toast(message,error=false){ const el=document.querySelector('#toast'); el.textContent=message; el.className='toast show'+(error?' error':''); clearTimeout(toast.timer); toast.timer=setTimeout(()=>el.className='toast',4200); }
function busy(show,title='Forging geometry',text='Running mesh repair, slicing and boolean joint construction…'){ document.querySelector('#progressTitle').textContent=title; document.querySelector('#progressText').textContent=text; document.querySelector('#progress').classList.toggle('show',show); }
function vectorPayload(v){ return {x:v.x,y:v.y,z:v.z}; }

async function api(url,options={}){ const response=await fetch(url,options); if(!response.ok){ let detail=response.statusText; try{ detail=(await response.json()).detail||detail; }catch{} throw new Error(detail); } return response.json(); }

const loader = new STLLoader();
function showGeometry(arrayBuffer){
  const geometry=loader.parse(arrayBuffer); geometry.computeVertexNormals(); geometry.center();
  if(state.modelMesh) scene.remove(state.modelMesh);
  const material=new THREE.MeshStandardMaterial({color:0xbfc9d8,roughness:.48,metalness:.18,side:THREE.DoubleSide});
  const mesh=new THREE.Mesh(geometry,material); mesh.castShadow=true; mesh.receiveShadow=true; scene.add(mesh); state.modelMesh=mesh;
  document.querySelector('#emptyState').classList.add('hidden'); clearMarker(); fitView();
}
function fitView(){ if(!state.modelMesh)return; const box=new THREE.Box3().setFromObject(state.modelMesh); const size=box.getSize(new THREE.Vector3()); const center=box.getCenter(new THREE.Vector3()); const max=Math.max(size.x,size.y,size.z); controls.target.copy(center); camera.position.copy(center.clone().add(new THREE.Vector3(max*1.35,max*.95,max*1.35))); camera.near=Math.max(.01,max/1000); camera.far=max*100; camera.updateProjectionMatrix(); controls.update(); }

async function upload(file){ busy(true,'Preparing model','Repairing the STL and measuring its printable geometry…'); try{
  const form=new FormData(); form.append('file',file); const data=await api('/api/upload',{method:'POST',body:form}); resetShireJob(); state.modelId=data.model_id; state.modelName=data.original_name;
  const modelResponse=await fetch(data.model_url); showGeometry(await modelResponse.arrayBuffer()); renderAnalysis(data.analysis); document.querySelector('#outputName').value=(file.name.replace(/\.stl$/i,'')+'_fidget').replace(/[^a-z0-9_-]+/gi,'_'); setButtons(); toast('Model repaired and loaded. Click the area that should move.');
 }catch(e){ toast(e.message,true); }finally{busy(false);} }

function renderAnalysis(a){
  document.querySelector('#faces').textContent=a.faces.toLocaleString(); document.querySelector('#watertight').textContent=a.watertight?'Yes':'Needs care';
  document.querySelector('#sizeX').textContent=a.extents_mm[0]+' mm'; document.querySelector('#sizeY').textContent=a.extents_mm[1]+' mm'; document.querySelector('#sizeZ').textContent=a.extents_mm[2]+' mm';
  document.querySelector('#volume').textContent=a.volume_mm3?Math.round(a.volume_mm3).toLocaleString()+' mm³':'Open mesh';
  const notes=[...(a.repair_notes||[])]; if(a.suggested_scale_warning)notes.push(a.suggested_scale_warning); document.querySelector('#repairNotes').textContent=notes.join(' ')||'No obvious repair problems were found.';
}

function clearMarker(){ if(state.markerObject){scene.remove(state.markerObject);state.markerObject=null;} state.marker=null;state.candidates=[];state.selectedType='auto'; document.querySelector('#markerList').innerHTML=''; document.querySelector('#candidates').innerHTML='<div class="notice">Place a joint marker to generate ranked engineering options.</div>'; document.querySelector('#downloadSection').style.display='none';setButtons(); }
function setButtons(){
  const hasModel=!!state.modelId, hasMarker=!!state.marker;
  document.querySelector('#fitBtn').disabled=!hasModel;
  document.querySelector('#clearBtn').disabled=!hasMarker;
  document.querySelector('#analyseBtn').disabled=!hasMarker;
  document.querySelector('#forgeBtn').disabled=!hasMarker||!!state.shireJob;
  document.querySelector('#askShireBtn').disabled=!hasModel;
  updateShireControls();
}

renderer.domElement.addEventListener('pointerdown',(event)=>{
  if(state.mode!=='joint'||!state.modelMesh||event.button!==0||event.ctrlKey||event.metaKey)return;
  const rect=renderer.domElement.getBoundingClientRect(); pointer.x=((event.clientX-rect.left)/rect.width)*2-1; pointer.y=-((event.clientY-rect.top)/rect.height)*2+1; raycaster.setFromCamera(pointer,camera);
  const hit=raycaster.intersectObject(state.modelMesh,false)[0]; if(!hit)return;
  if(state.shireJob){resetShireJob();toast('Manual joint placement switched back to standalone mode.');}
  const localPoint=state.modelMesh.worldToLocal(hit.point.clone()); const normal=hit.face.normal.clone().transformDirection(state.modelMesh.matrixWorld);
  placeMarker(localPoint,normal); analyseJoint();
});
function placeMarker(point,normal){ clearMarker(); state.marker={point,normal}; const radius=Math.max(1.8,new THREE.Box3().setFromObject(state.modelMesh).getSize(new THREE.Vector3()).length()/140); const sphere=new THREE.Mesh(new THREE.SphereGeometry(radius,28,18),new THREE.MeshStandardMaterial({color:0xff8b3d,emissive:0x5a1700,emissiveIntensity:1.5})); sphere.position.copy(point); scene.add(sphere);state.markerObject=sphere;
 document.querySelector('#markerList').innerHTML=`<div class="marker-card"><span class="marker-dot"></span><div><strong>Joint 01</strong><br><small>${point.x.toFixed(1)}, ${point.y.toFixed(1)}, ${point.z.toFixed(1)} mm</small></div></div>`;setButtons(); }

function jointPayload(){ return { point:vectorPayload(state.marker.point), surface_normal:vectorPayload(state.marker.normal), movement:document.querySelector('#movement').value, requested_type:state.selectedType, clearance_mm:Number(document.querySelector('#clearance').value), strength_bias:.7 }; }
async function analyseJoint(){ if(!state.marker)return; try{ const data=await api('/api/analyse-joint',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model_id:state.modelId,joint:jointPayload()})}); state.candidates=data.candidates; if(state.selectedType==='auto')state.selectedType=data.candidates[0].type; renderCandidates(); if(data.warning)toast(data.warning,true); }catch(e){toast(e.message,true);} }
function renderCandidates(){ const container=document.querySelector('#candidates'); container.innerHTML=''; state.candidates.forEach(c=>{ const card=document.createElement('div');card.className='candidate'+(state.selectedType===c.type?' selected':''); card.innerHTML=`<div class="candidate-head"><h3>${c.name}</h3><span class="score">${c.score}%</span></div><p>${c.reason}</p><div class="tags"><span class="tag">${c.movement}</span><span class="tag">${c.print_mode}</span><span class="tag">Ø ${c.radius_mm} mm</span></div>`;card.onclick=async()=>{
      if(state.shireJob&&!['AWAITING_DESIGN_SELECTION','AWAITING_DESIGN_APPROVAL'].includes(state.shireJob.state)){toast('This SHiRE design is already approved or forged. Start a new job to change the mechanism.',true);return;}
      state.selectedType=c.type;renderCandidates();
      if(state.shireJob?.selected_joint){
        try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/select`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({suggestion_id:state.shireJob.selected_joint.suggestion_id,requested_type:c.type})});state.designToken=null;state.exportToken=null;document.querySelector('#designToken').textContent='No design token';document.querySelector('#exportToken').textContent='No export token';document.querySelector('#shireGateMessage').textContent=`Mechanism changed to ${c.name}. A fresh design approval is required.`;updateShireControls();}
        catch(e){toast(e.message,true);}
      }
    };container.appendChild(card); }); }

async function forge(){ if(!state.marker)return; busy(true); try{ const payload={model_id:state.modelId,joint:jointPayload(),output_name:document.querySelector('#outputName').value||'flexiforge_output',layout_gap_mm:10}; const result=await api('/api/forge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); document.querySelector('#downloadSection').style.display='block';document.querySelector('#downloadSummary').textContent=`${result.report.joint_type.replace('_',' ')} · ${result.report.clearance_mm} mm clearance · includes print layout and engineering report.`;document.querySelector('#downloadLink').href=result.download_url; toast('Fidget package forged successfully.');
    const preview=await fetch(result.preview_url); if(preview.ok)showForgedPreview(await preview.arrayBuffer());
  }catch(e){toast(e.message,true);}finally{busy(false);} }
function showForgedPreview(buffer){ const geometry=loader.parse(buffer);geometry.computeVertexNormals();geometry.center(); if(state.modelMesh)scene.remove(state.modelMesh);if(state.markerObject)scene.remove(state.markerObject); const material=new THREE.MeshStandardMaterial({color:0xffa25f,roughness:.4,metalness:.12,side:THREE.DoubleSide});state.modelMesh=new THREE.Mesh(geometry,material);scene.add(state.modelMesh);state.markerObject=null;fitView(); }


function resetShireJob(){
  state.shireJob=null; state.designToken=null; state.exportToken=null;
  document.querySelector('#shireSuggestions').innerHTML='';
  document.querySelector('#shireJobId').textContent='Not started';
  document.querySelector('#shireState').textContent='—';
  document.querySelector('#designToken').textContent='No design token';
  document.querySelector('#exportToken').textContent='No export token';
  document.querySelector('#vaultResult').textContent='Final export is locked to a verified SHiREVault network mount.';
  document.querySelector('#shireGateMessage').textContent='Ask SHiRE for suggestions, then select one joint design.';
  updateShireControls();
}

function updateShireControls(){
  const job=state.shireJob; const status=job?.state||'';
  document.querySelector('#shireJobId').textContent=job?.job_id||'Not started';
  document.querySelector('#shireState').textContent=status||'—';
  document.querySelector('#requestDesignBtn').disabled=status!=='AWAITING_DESIGN_APPROVAL';
  document.querySelector('#approveDesignBtn').disabled=!(status==='AWAITING_DESIGN_APPROVAL'&&state.designToken);
  document.querySelector('#shireForgeBtn').disabled=status!=='APPROVED_FOR_FORGE';
  document.querySelector('#shireValidateBtn').disabled=status!=='FORGED';
  document.querySelector('#requestExportBtn').disabled=status!=='VALIDATED';
  document.querySelector('#approveExportBtn').disabled=!(status==='AWAITING_EXPORT_APPROVAL'&&state.exportToken);
  document.querySelector('#vaultExportBtn').disabled=status!=='APPROVED_FOR_EXPORT';
}

function renderShireSuggestions(){
  const container=document.querySelector('#shireSuggestions'); container.innerHTML='';
  if(!state.shireJob?.suggestions?.length){container.innerHTML='<div class="notice">No automatic suggestions were generated.</div>';return;}
  state.shireJob.suggestions.forEach(s=>{
    const card=document.createElement('button'); card.className='shire-suggestion';
    card.innerHTML=`<span><strong>${s.label}</strong><small>${s.recommended_joint.name} · ${s.local_radius_mm} mm radius</small></span><b>${s.confidence}%</b>`;
    card.onclick=()=>selectShireSuggestion(s);
    container.appendChild(card);
  });
}

async function askShire(){
  if(!state.modelId)return;
  busy(true,'SHiRE is analysing the model','Generating deterministic joint locations and ranking printable mechanisms…');
  try{
    const payload={model_id:state.modelId,project_name:document.querySelector('#shireProject').value||'SHiRE FlexiForge Project',intent:document.querySelector('#shireIntent').value||'Turn this STL into an articulated fidget.',movement:document.querySelector('#movement').value,clearance_mm:Number(document.querySelector('#clearance').value),max_suggestions:5};
    state.shireJob=await api('/api/shire/jobs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
    state.designToken=null; state.exportToken=null; renderShireSuggestions(); updateShireControls();
    document.querySelector('#shireGateMessage').textContent='SHiRE generated ranked locations. Select one to place it on the model and begin approval.';
    toast('SHiRE generated joint suggestions. Choose the location you want.');
  }catch(e){toast(e.message,true);}finally{busy(false);}
}

async function selectShireSuggestion(s){
  try{
    state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/select`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({suggestion_id:s.suggestion_id,requested_type:'auto'})});
    state.designToken=null; state.exportToken=null;
    const point=new THREE.Vector3(s.point.x,s.point.y,s.point.z); const normal=new THREE.Vector3(s.cut_normal.x,s.cut_normal.y,s.cut_normal.z);
    placeMarker(point,normal); state.selectedType=s.recommended_joint.type; await analyseJoint();
    document.querySelector('#shireGateMessage').textContent=`Selected ${s.label}: ${s.recommended_joint.name}. Geometry is still locked.`;
    updateShireControls();
  }catch(e){toast(e.message,true);}
}

async function requestDesignApproval(){
  try{const data=await api(`/api/shire/jobs/${state.shireJob.job_id}/request-design-approval`,{method:'POST'});state.designToken=data.approval_token;document.querySelector('#designToken').textContent=data.approval_token;document.querySelector('#shireGateMessage').textContent='Review the selected location and mechanism. Approving binds this exact design hash.';updateShireControls();}
  catch(e){toast(e.message,true);}
}
async function approveDesign(){
  try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/approve-design`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:state.designToken})});document.querySelector('#shireGateMessage').textContent='Design approved. SHiRE may now ask FlexiForge to generate geometry locally.';updateShireControls();toast('Design approval recorded.');}
  catch(e){toast(e.message,true);}
}
async function shireForge(){
  busy(true,'SHiRE is forging the approved design','Building real articulation geometry. Export remains locked.');
  try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/forge`,{method:'POST'});document.querySelector('#shireGateMessage').textContent='Geometry generated locally. Validation is required before export approval.';updateShireControls();toast('Approved design forged locally.');
    const preview=await fetch(`/api/shire/jobs/${state.shireJob.job_id}/preview`); if(preview.ok)showForgedPreview(await preview.arrayBuffer());
  }catch(e){toast(e.message,true);}finally{busy(false);}
}
async function shireValidate(){
  busy(true,'Validating FlexiForge output','Checking every generated STL, dimensions, hashes and watertightness…');
  try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/validate`,{method:'POST'});const result=state.shireJob.validation;document.querySelector('#shireGateMessage').textContent=`Validation: ${result.status}. ${result.warnings.length?result.warnings.join(' '):'No automatic geometry warnings.'}`;updateShireControls();toast(`Validation complete: ${result.status}`);}
  catch(e){toast(e.message,true);}finally{busy(false);}
}
async function requestExportApproval(){
  try{const data=await api(`/api/shire/jobs/${state.shireJob.job_id}/request-export-approval`,{method:'POST'});state.exportToken=data.approval_token;state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}`);document.querySelector('#exportToken').textContent=data.approval_token;document.querySelector('#shireGateMessage').textContent='Review validation results. This approval authorises only the current verified package.';updateShireControls();}
  catch(e){toast(e.message,true);}
}
async function approveExport(){
  try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/approve-export`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:state.exportToken})});document.querySelector('#shireGateMessage').textContent='Export approved. FlexiForge will still verify the SHiREVault network mount before copying anything.';updateShireControls();toast('Export approval recorded.');}
  catch(e){toast(e.message,true);}
}
async function vaultExport(){
  busy(true,'Exporting to SHiREVault','Verifying the network mount, copying the package and hashing every exported file…');
  try{state.shireJob=await api(`/api/shire/jobs/${state.shireJob.job_id}/export`,{method:'POST'});document.querySelector('#shireGateMessage').textContent='Export complete and verified on SHiREVault.';document.querySelector('#vaultResult').textContent=state.shireJob.export.destination;updateShireControls();toast('Verified SHiREVault export complete.');}
  catch(e){toast(e.message,true);}finally{busy(false);}
}

const drop=document.querySelector('#dropzone'), input=document.querySelector('#fileInput');drop.onclick=()=>input.click();input.onchange=()=>input.files[0]&&upload(input.files[0]);['dragenter','dragover'].forEach(n=>drop.addEventListener(n,e=>{e.preventDefault();drop.classList.add('drag')}));['dragleave','drop'].forEach(n=>drop.addEventListener(n,e=>{e.preventDefault();drop.classList.remove('drag')}));drop.addEventListener('drop',e=>e.dataTransfer.files[0]&&upload(e.dataTransfer.files[0]));
document.querySelector('#clearBtn').onclick=clearMarker;document.querySelector('#fitBtn').onclick=fitView;document.querySelector('#analyseBtn').onclick=analyseJoint;document.querySelector('#forgeBtn').onclick=forge;document.querySelector('#askShireBtn').onclick=askShire;document.querySelector('#requestDesignBtn').onclick=requestDesignApproval;document.querySelector('#approveDesignBtn').onclick=approveDesign;document.querySelector('#shireForgeBtn').onclick=shireForge;document.querySelector('#shireValidateBtn').onclick=shireValidate;document.querySelector('#requestExportBtn').onclick=requestExportApproval;document.querySelector('#approveExportBtn').onclick=approveExport;document.querySelector('#vaultExportBtn').onclick=vaultExport;document.querySelector('#movement').onchange=()=>state.marker&&analyseJoint();document.querySelector('#clearance').oninput=e=>{document.querySelector('#clearanceValue').textContent=Number(e.target.value).toFixed(2)+' mm';};document.querySelector('#clearance').onchange=()=>state.marker&&analyseJoint();
document.querySelector('#selectTool').onclick=()=>{state.mode='joint';document.querySelector('#selectTool').classList.add('active');document.querySelector('#orbitTool').classList.remove('active');document.querySelector('#modeChip').textContent='Mode: Place joint';};document.querySelector('#orbitTool').onclick=()=>{state.mode='orbit';document.querySelector('#orbitTool').classList.add('active');document.querySelector('#selectTool').classList.remove('active');document.querySelector('#modeChip').textContent='Mode: Orbit model';};
fetch('/api/health').catch(()=>{document.querySelector('#serverStatus').textContent='Geometry engine unavailable';document.querySelector('.status-dot').style.background='var(--danger)';});
