robopark 3.3.44 → 3.3.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,57 +2,57 @@
2
2
  "/_not-found/page": "app/_not-found/page.js",
3
3
  "/api/device-character/route": "app/api/device-character/route.js",
4
4
  "/api/download-piper-model/route": "app/api/download-piper-model/route.js",
5
- "/api/prompt/route": "app/api/prompt/route.js",
6
- "/api/reload-tools/route": "app/api/reload-tools/route.js",
7
5
  "/api/models/route": "app/api/models/route.js",
6
+ "/api/prompt/route": "app/api/prompt/route.js",
8
7
  "/api/prewarm/route": "app/api/prewarm/route.js",
9
8
  "/api/robopark/devices/[deviceId]/control/route": "app/api/robopark/devices/[deviceId]/control/route.js",
10
9
  "/api/robopark/robots/[robotId]/operations/route": "app/api/robopark/robots/[robotId]/operations/route.js",
10
+ "/api/reload-tools/route": "app/api/reload-tools/route.js",
11
11
  "/api/robopark/sessions/[sessionId]/end/route": "app/api/robopark/sessions/[sessionId]/end/route.js",
12
12
  "/api/robopark/transcripts/[sessionId]/route": "app/api/robopark/transcripts/[sessionId]/route.js",
13
- "/api/setup/status/route": "app/api/setup/status/route.js",
14
13
  "/api/setup/complete/route": "app/api/setup/complete/route.js",
15
14
  "/api/setup/test-groq/route": "app/api/setup/test-groq/route.js",
16
15
  "/api/setup/test-hass/route": "app/api/setup/test-hass/route.js",
17
- "/api/setup/test-ollama/route": "app/api/setup/test-ollama/route.js",
16
+ "/api/setup/status/route": "app/api/setup/status/route.js",
18
17
  "/api/setup/test-n8n/route": "app/api/setup/test-n8n/route.js",
18
+ "/api/setup/test-ollama/route": "app/api/setup/test-ollama/route.js",
19
19
  "/api/test-tts/route": "app/api/test-tts/route.js",
20
20
  "/api/voices/route": "app/api/voices/route.js",
21
21
  "/api/wake-word/models/route": "app/api/wake-word/models/route.js",
22
22
  "/api/wake-word/status/route": "app/api/wake-word/status/route.js",
23
23
  "/api/wake-word/upload/route": "app/api/wake-word/upload/route.js",
24
24
  "/favicon.ico/route": "app/favicon.ico/route.js",
25
- "/api/model-status/route": "app/api/model-status/route.js",
26
25
  "/api/kiosk-hardware/route": "app/api/kiosk-hardware/route.js",
27
- "/api/robopark/character-ai/route": "app/api/robopark/character-ai/route.js",
28
26
  "/api/connection-details/route": "app/api/connection-details/route.js",
27
+ "/api/model-status/route": "app/api/model-status/route.js",
28
+ "/api/robopark/device-access/route": "app/api/robopark/device-access/route.js",
29
29
  "/api/robopark/camera-token/route": "app/api/robopark/camera-token/route.js",
30
- "/api/robopark/robots/route": "app/api/robopark/robots/route.js",
31
30
  "/api/robopark/live-rooms/route": "app/api/robopark/live-rooms/route.js",
32
- "/api/robopark/device-access/route": "app/api/robopark/device-access/route.js",
33
31
  "/api/robopark/monitor-token/route": "app/api/robopark/monitor-token/route.js",
32
+ "/api/robopark/character-ai/route": "app/api/robopark/character-ai/route.js",
33
+ "/api/robopark/robots/route": "app/api/robopark/robots/route.js",
34
34
  "/api/robopark/servers/[serverId]/metrics/route": "app/api/robopark/servers/[serverId]/metrics/route.js",
35
+ "/api/robopark/servers/[serverId]/route": "app/api/robopark/servers/[serverId]/route.js",
35
36
  "/api/robopark/servers/route": "app/api/robopark/servers/route.js",
36
37
  "/api/robopark/sessions/route": "app/api/robopark/sessions/route.js",
37
- "/api/robopark/servers/[serverId]/route": "app/api/robopark/servers/[serverId]/route.js",
38
- "/api/robopark/vision-caption/route": "app/api/robopark/vision-caption/route.js",
39
- "/api/robopark/robots/[robotId]/voice-engine/route": "app/api/robopark/robots/[robotId]/voice-engine/route.js",
40
38
  "/api/robovoice/robots/[robotId]/media-health/route": "app/api/robovoice/robots/[robotId]/media-health/route.js",
39
+ "/api/robopark/vision-caption/route": "app/api/robopark/vision-caption/route.js",
41
40
  "/api/settings/route": "app/api/settings/route.js",
42
- "/api/robopark/transcripts/route": "app/api/robopark/transcripts/route.js",
41
+ "/api/robopark/robots/[robotId]/voice-engine/route": "app/api/robopark/robots/[robotId]/voice-engine/route.js",
43
42
  "/api/turn-credentials/route": "app/api/turn-credentials/route.js",
44
- "/api/wake/route": "app/api/wake/route.js",
43
+ "/api/robopark/transcripts/route": "app/api/robopark/transcripts/route.js",
45
44
  "/camera-grid/route": "app/camera-grid/route.js",
46
- "/robopark/[...path]/route": "app/robopark/[...path]/route.js",
45
+ "/api/wake/route": "app/api/wake/route.js",
47
46
  "/fed/media/robots/[robotId]/video_feed/route": "app/fed/media/robots/[robotId]/video_feed/route.js",
48
- "/robopark/api/robots/[robotId]/stream/route": "app/robopark/api/robots/[robotId]/stream/route.js",
49
- "/fed/[...path]/route": "app/fed/[...path]/route.js",
50
- "/tailnet/route": "app/tailnet/route.js",
47
+ "/robopark/[...path]/route": "app/robopark/[...path]/route.js",
51
48
  "/vendor/livekit-client.umd.js/route": "app/vendor/livekit-client.umd.js/route.js",
49
+ "/fed/[...path]/route": "app/fed/[...path]/route.js",
50
+ "/robopark/api/robots/[robotId]/stream/route": "app/robopark/api/robots/[robotId]/stream/route.js",
52
51
  "/(app)/opengraph-image-xg4ifa/route": "app/(app)/opengraph-image-xg4ifa/route.js",
52
+ "/tailnet/route": "app/tailnet/route.js",
53
53
  "/robopark/api/[...path]/route": "app/robopark/api/[...path]/route.js",
54
54
  "/robopark/page": "app/robopark/page.js",
55
55
  "/voice-tester/page": "app/voice-tester/page.js",
56
- "/(app)/voice/page": "app/(app)/voice/page.js",
57
- "/(app)/page": "app/(app)/page.js"
56
+ "/(app)/page": "app/(app)/page.js",
57
+ "/(app)/voice/page": "app/(app)/voice/page.js"
58
58
  }
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/><title data-next-head="">500: Internal Server Error</title><noscript data-n-css=""></noscript><script defer="" noModule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-5252e5254da079b5.js" defer=""></script><script src="/_next/static/chunks/framework-e60c938074ff7136.js" defer=""></script><script src="/_next/static/chunks/main-9e24e8708ff075fc.js" defer=""></script><script src="/_next/static/chunks/pages/_app-131c90850aef965b.js" defer=""></script><script src="/_next/static/chunks/pages/_error-e4ba546eb376bdf4.js" defer=""></script><script src="/_next/static/VHNdBIGajqd6Wv28naVNO/_buildManifest.js" defer=""></script><script src="/_next/static/VHNdBIGajqd6Wv28naVNO/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">500</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">Internal Server Error<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":500}},"page":"/_error","query":{},"buildId":"VHNdBIGajqd6Wv28naVNO","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/><title data-next-head="">500: Internal Server Error</title><noscript data-n-css=""></noscript><script defer="" noModule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-5252e5254da079b5.js" defer=""></script><script src="/_next/static/chunks/framework-e60c938074ff7136.js" defer=""></script><script src="/_next/static/chunks/main-9e24e8708ff075fc.js" defer=""></script><script src="/_next/static/chunks/pages/_app-131c90850aef965b.js" defer=""></script><script src="/_next/static/chunks/pages/_error-e4ba546eb376bdf4.js" defer=""></script><script src="/_next/static/GwsTXhcc2wWhmMKp14ZmG/_buildManifest.js" defer=""></script><script src="/_next/static/GwsTXhcc2wWhmMKp14ZmG/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">500</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">Internal Server Error<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":500}},"page":"/_error","query":{},"buildId":"GwsTXhcc2wWhmMKp14ZmG","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
@@ -34,7 +34,7 @@
34
34
  callPanel.insertAdjacentHTML('beforeend','<div class=call-row hidden><label for=call-mode>Call mode</label><select id=call-mode><option value=test>Test call · browser only</option><option value=production>Production on-site · robot video and tools</option></select></div>');
35
35
  callPanel.insertAdjacentHTML('beforeend','<div class=call-row id=robot-row hidden><label for=call-robot>Production robot</label><select id=call-robot><option value=none>Select a registered robot...</option></select></div>');
36
36
  callPanel.insertAdjacentHTML('beforeend','<div class=call-row><label for=call-microphone>Microphone</label><select id=call-microphone><option value=default>System default microphone</option></select></div>');
37
- callPanel.insertAdjacentHTML('beforeend','<div class=call-row hidden><label for=call-camera>Camera</label><select id=call-camera><option value=default>System default camera</option></select></div>');
37
+ callPanel.insertAdjacentHTML('beforeend','<div class=call-row id=camera-row hidden><label for=call-camera id=camera-label>Camera</label><select id=call-camera><option value=default>System default camera</option></select></div>');
38
38
  callPanel.insertAdjacentHTML('beforeend','<div class=call-row><label for=call-speaker>Speaker output</label><select id=call-speaker><option value=default>System default speaker</option></select></div>');
39
39
  callPanel.insertAdjacentHTML('beforeend','<div class=call-actions hidden><button class=primary id=request-hardware type=button>Request all media</button><button id=refresh-hardware type=button>Refresh devices</button></div>');
40
40
  callPanel.insertAdjacentHTML('beforeend','<p class="call-note call-status" id=hardware-status>Grant browser media access to reveal device names.</p><span class=mode-badge id=mode-badge hidden>TEST ? BROWSER ONLY</span>');
@@ -52,6 +52,96 @@
52
52
  : 'Microphone and camera are blocked because this page is not on a secure origin ('+location.protocol+'//'+location.host+'). Open it over https:// or from localhost.';
53
53
  }
54
54
  function refreshHardware(requestPermission){if(!navigator.mediaDevices||!navigator.mediaDevices.enumerateDevices){setHardwareStatus(mediaBlockedReason(),'bad');return Promise.reject(new Error('media devices unavailable'))}var permission=Promise.resolve();if(requestPermission){permission=navigator.mediaDevices.getUserMedia({audio:true,video:true}).catch(function(){return navigator.mediaDevices.getUserMedia({audio:true})}).then(function(stream){stream.getTracks().forEach(function(track){track.stop()})})}setHardwareStatus(requestPermission?'Requesting browser media access...':'Scanning browser hardware...','');return permission.then(function(){return navigator.mediaDevices.enumerateDevices()}).then(function(devices){fillHardwareSelect('call-microphone',devices,'audioinput','microphone');fillHardwareSelect('call-camera',devices,'videoinput','camera');fillHardwareSelect('call-speaker',devices,'audiooutput','speaker');var mic=devices.filter(function(device){return device.kind==='audioinput'}).length,cam=devices.filter(function(device){return device.kind==='videoinput'}).length,out=devices.filter(function(device){return device.kind==='audiooutput'}).length;setHardwareStatus('Ready: '+mic+' microphone(s), '+cam+' camera(s), '+out+' speaker output(s).','ok');return devices}).catch(function(error){setHardwareStatus(error&&error.message||String(error),'bad');throw error})}
55
+ // ---- Persistent device selection ---------------------------------------
56
+ // Two consumers read this page's pick and they speak different identifiers,
57
+ // so both are written on every change:
58
+ // * PUT '/robopark'/api/kiosk-hardware stores browser MediaDeviceInfo.deviceIds,
59
+ // which is what lets this page and the control-centre panel restore
60
+ // themselves after a reload.
61
+ // * PATCH '/robopark'/api/devices/<robot> stores the option LABEL, because the
62
+ // OpenCV/sounddevice side can only act on a human-readable name; a
63
+ // browser deviceId is origin-scoped and rotates, so it is useless to
64
+ // Python. That row is exactly what `robopark start` replays.
65
+ // Writing only the first is why the CLI was left scanning for a camera while
66
+ // this page worked every time.
67
+ var HW_SELECTS={audioinput:'call-microphone',videoinput:'call-camera',audiooutput:'call-speaker'};
68
+ var HW_KEYS={audioinput:'robopark:media-device:audioinput',videoinput:'robopark:media-device:videoinput',audiooutput:'robopark:media-device:audiooutput'};
69
+ var HW_DEVICE_FIELD={audioinput:'audio_device',videoinput:'video_device',audiooutput:'audio_output_device'};
70
+ var HW_NOUN={audioinput:'microphone',videoinput:'camera',audiooutput:'speaker'};
71
+ var hardwareRevision=0;
72
+ function hardwareValue(kind){var select=el(HW_SELECTS[kind]);return select&&select.value&&select.value!=='default'?select.value:''}
73
+ // Chrome suffixes device labels with the USB vendor:product pair
74
+ // ("HD Webcam C270 (046d:0825)"), which the OS name never carries — and the
75
+ // resolver on the other side matches by substring of the OS name, so the
76
+ // suffix turns a good label into no match at all.
77
+ function hardwareLabel(kind){var select=el(HW_SELECTS[kind]);if(!select||select.value==='default'||select.selectedIndex<0)return '';return String((select.options[select.selectedIndex]||{}).textContent||'').replace(/\s*\([0-9a-f]{4}:[0-9a-f]{4}\)\s*$/i,'').trim()}
78
+ function hardwareSelection(){return {audioinput:hardwareValue('audioinput'),videoinput:hardwareValue('videoinput'),audiooutput:hardwareValue('audiooutput')}}
79
+ function storeHardwareLocal(kind){try{var value=hardwareValue(kind);if(value)localStorage.setItem(HW_KEYS[kind],value);else localStorage.removeItem(HW_KEYS[kind])}catch(_){}}
80
+ function pushHardwareSelection(kind){
81
+ storeHardwareLocal(kind);
82
+ if(!robot)return Promise.resolve();
83
+ var selection=hardwareSelection(),label=hardwareLabel(kind),body={};
84
+ // An empty pick means "you decide": for the camera that is the literal
85
+ // device value 'auto' (scan every index), for audio it is the empty string
86
+ // the vision server already reads as the system default.
87
+ body[HW_DEVICE_FIELD[kind]]=label||(kind==='videoinput'?'auto':'');
88
+ var writes=[fetch('/robopark'+'/api/devices/'+encodeURIComponent(robot),{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify(body)}).catch(function(){})];
89
+ // PUT /api/kiosk-hardware answers 400 when the microphone is empty, so the
90
+ // browser half of the write waits until one is actually chosen.
91
+ if(selection.audioinput)writes.push(fetch('/robopark'+'/api/kiosk-hardware',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({device_id:robot,selection:selection})}).then(function(response){return response.ok?response.json():null}).then(function(data){if(data&&data.hardware)hardwareRevision=Number(data.hardware.revision||0)}).catch(function(){}));
92
+ // Under ?camera=off the vision server owns the capture device, so the pick
93
+ // is also handed to it directly and takes effect now rather than at the
94
+ // next `robopark start`. Best-effort: this is a cross-origin call to
95
+ // 127.0.0.1 and a page served over https cannot make it.
96
+ if(kind==='videoinput'&&cameraOff&&visionBase)writes.push(fetch(visionBase+'/api/media/config',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({video_device:label||'auto'})}).catch(function(){}));
97
+ return Promise.all(writes).then(function(){setHardwareStatus('Saved '+HW_NOUN[kind]+': '+(label||'system default')+' - kept for this kiosk and replayed by `robopark start`.','ok')})
98
+ }
99
+ // Echo the enumerated devices back. The revision/applied_revision handshake
100
+ // is what the control-centre hardware panel reads; a page that PUT a
101
+ // selection but never reported would leave that panel permanently showing a
102
+ // pending change.
103
+ function reportHardwareObserved(devices){
104
+ if(!robot||!devices)return Promise.resolve(devices);
105
+ return fetch('/robopark'+'/api/kiosk-hardware',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
106
+ device_id:robot,
107
+ devices:Array.prototype.slice.call(devices).filter(function(device){return device.deviceId}).map(function(device){return {deviceId:device.deviceId,kind:device.kind,label:device.label}}),
108
+ selected:hardwareSelection(),
109
+ secure:Boolean(window.isSecureContext),
110
+ applied_revision:hardwareRevision
111
+ })}).catch(function(){}).then(function(){return devices})
112
+ }
113
+ function scanHardware(requestPermission){return refreshHardware(requestPermission).then(reportHardwareObserved)}
114
+ function applyHardwareValue(kind){var value=hardwareValue(kind);if(kind==='audioinput')inputDeviceId=value;else if(kind==='videoinput')cameraDeviceId=value;else outputDeviceId=value}
115
+ // Seed precedence matches the Next hardware gate: the scheduler's desired
116
+ // pick wins, then whatever the kiosk last reported, then this browser's own
117
+ // localStorage. `dataset.dirty` is what stops a seed that lands late from
118
+ // clobbering a pick the operator has already made.
119
+ function seedHardwareFromScheduler(){
120
+ if(!robot)return Promise.resolve();
121
+ return fetch('/robopark'+'/api/kiosk-hardware?device_id='+encodeURIComponent(robot),{cache:'no-store'}).then(function(response){return response.ok?response.json():null}).then(function(data){
122
+ var hardware=(data&&data.hardware)||{},desired=hardware.desired||{},observed=hardware.observed||{},reported=observed.selected||{};
123
+ hardwareRevision=Number(hardware.revision||0);
124
+ Object.keys(HW_SELECTS).forEach(function(kind){
125
+ var select=el(HW_SELECTS[kind]);if(!select||select.dataset.dirty==='1')return;
126
+ var wanted=desired[kind]||reported[kind]||'';
127
+ if(!wanted){try{wanted=localStorage.getItem(HW_KEYS[kind])||''}catch(_){}}
128
+ if(wanted&&Array.prototype.some.call(select.options,function(option){return option.value===wanted}))select.value=wanted;
129
+ applyHardwareValue(kind)
130
+ })
131
+ }).catch(function(){})
132
+ }
133
+ // ?camera=off means this page never calls getUserMedia({video}) — but the
134
+ // operator still has to be able to say WHICH camera the vision server should
135
+ // open, which is the choice `robopark start` was previously making by
136
+ // scanning indices. So on a kiosk the picker is revealed and relabelled
137
+ // rather than left hidden.
138
+ function revealVisionCameraPicker(){
139
+ if(!(q.get('kiosk')==='1'||q.get('kiosk')==='on')&&!cameraOff)return;
140
+ var row=el('camera-row'),label=el('camera-label');
141
+ if(row)row.hidden=false;
142
+ if(label)label.textContent=cameraOff?'Vision server camera':'Camera';
143
+ if(cameraOff)callPanel.insertAdjacentHTML('beforeend','<p class="call-note">This picks the camera the on-site vision server opens for the robot feed. The browser does not open a local preview here.</p>')
144
+ }
55
145
  function ensureCameraSdk(){if(window.LivekitClient)return Promise.resolve(window.LivekitClient);if(cameraSdkPromise)return cameraSdkPromise;cameraSdkPromise=new Promise(function(resolve,reject){var script=document.createElement('script');script.src='/vendor/livekit-client.umd.js';script.onload=function(){window.LivekitClient?resolve(window.LivekitClient):reject(new Error('LiveKit camera SDK unavailable'))};script.onerror=function(){reject(new Error('LiveKit camera SDK failed to load'))};document.head.appendChild(script)});return cameraSdkPromise}
56
146
  function scheduleCameraPublishRetry(){if(cameraPublishRetry||ending||!localVideoStream)return;if(cameraRetryCount>=8){event('camera_published','failed','Giving up after 8 attempts - the media server is not reachable.',{attempts:cameraRetryCount});return}var delay=Math.min(30000,3000*Math.pow(1.6,cameraRetryCount));cameraRetryCount++;cameraPublishRetry=setTimeout(function(){cameraPublishRetry=null;publishLocalVideo().catch(function(){})},delay)}
57
147
  function publishLocalVideo(){var pendingCameraRoom=null;if(!localVideoStream||!localVideoStream.getVideoTracks().length)return Promise.resolve();if(cameraRoom)return Promise.resolve();return Promise.all([ensureCameraSdk(),fetch('/api/robopark/camera-token',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({character_id:character,robot_id:robot||null})}).then(function(response){if(!response.ok)throw new Error('camera token '+response.status);return response.json()})]).then(function(parts){var LK=parts[0],info=parts[1],room=new LK.Room();pendingCameraRoom=room;cameraRoom=room;room.on(LK.RoomEvent.Disconnected,function(){if(cameraRoom===room){cameraRoom=null;scheduleCameraPublishRetry()}});return room.connect(info.url,info.token).then(function(){return room.localParticipant.publishTrack(localVideoStream.getVideoTracks()[0],{name:'camera',source:LK.Track.Source.Camera})}).then(function(){cameraRetryCount=0;if(cameraPublishRetry){clearTimeout(cameraPublishRetry);cameraPublishRetry=null}event('camera_published','ok','Camera relay live',{})})}).catch(function(error){try{if(pendingCameraRoom&&pendingCameraRoom.disconnect)pendingCameraRoom.disconnect(true)}catch(_){}pendingCameraRoom=null;cameraRoom=null;event('camera_published','failed',error.message,{});scheduleCameraPublishRetry();throw error})}
@@ -336,12 +426,13 @@
336
426
  el('call-mode').value=mode;el('call-robot').value=robot||'none';
337
427
  el('call-mode').onchange=function(){if(desired||conversation||connecting)end('mode_changed');applyCallMode()};
338
428
  el('call-robot').onchange=function(){robot=this.value==='none'?'':this.value;showRobotVideo();text('f-robot',robot||'missing robot')};
339
- el('call-microphone').onchange=function(){inputDeviceId=this.value==='default'?'':this.value};
340
- el('call-camera').onchange=function(){cameraDeviceId=this.value==='default'?'':this.value;startLocalVideo().catch(function(){})};
341
- el('call-speaker').onchange=function(){outputDeviceId=this.value==='default'?'':this.value};
342
- el('request-hardware').onclick=function(){refreshHardware(true).catch(function(){})};
343
- el('refresh-hardware').onclick=function(){refreshHardware(false).catch(function(){})};
344
- applyCallMode();loadProductionRobots();refreshHardware(false).catch(function(){});
429
+ el('call-microphone').onchange=function(){this.dataset.dirty='1';inputDeviceId=this.value==='default'?'':this.value;pushHardwareSelection('audioinput')};
430
+ el('call-camera').onchange=function(){this.dataset.dirty='1';cameraDeviceId=this.value==='default'?'':this.value;pushHardwareSelection('videoinput');startLocalVideo().catch(function(){})};
431
+ el('call-speaker').onchange=function(){this.dataset.dirty='1';outputDeviceId=this.value==='default'?'':this.value;pushHardwareSelection('audiooutput')};
432
+ el('request-hardware').onclick=function(){scanHardware(true).catch(function(){})};
433
+ el('refresh-hardware').onclick=function(){scanHardware(false).catch(function(){})};
434
+ revealVisionCameraPicker();
435
+ applyCallMode();loadProductionRobots();scanHardware(false).then(seedHardwareFromScheduler).catch(function(){});
345
436
  function installElevenLabsHardwareRouting(){var sdk=window.ElevenLabsClient;if(!sdk||!sdk.Conversation)return false;if(sdk.Conversation.__roboparkHardware)return true;var nativeStart=sdk.Conversation.startSession.bind(sdk.Conversation);sdk.Conversation.startSession=function(options){return nativeStart(Object.assign({},options||{},{inputDeviceId:inputDeviceId||undefined,outputDeviceId:outputDeviceId||undefined}))};sdk.Conversation.__roboparkHardware=true;return true}
346
437
  installElevenLabsHardwareRouting();
347
438
  var hardwarePatchTimer=setInterval(function(){if(installElevenLabsHardwareRouting())clearInterval(hardwarePatchTimer)},500);