slopfixing / issue 002

This commit is contained in:
legop3
2026-06-12 02:19:08 -04:00
parent be8c3d7b29
commit c0050498b2
36 changed files with 5841 additions and 299 deletions
@@ -1,3 +1,4 @@
# FIXED!
# Issue 001: Global Input Listener Churn # Issue 001: Global Input Listener Churn
## Summary ## Summary
@@ -0,0 +1,849 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-07-57-124Z-root-runtime",
"environment": {
"cpuThrottle": 6,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 10000
},
"metrics": {
"Timestamp": 23292.158609,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 398,
"LayoutObjects": 1945,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2266,
"Resources": 18,
"ContextLifecycleStateObservers": 13,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 0,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 3,
"LayoutCount": 12,
"RecalcStyleCount": 30,
"LayoutDuration": 0.928481,
"RecalcStyleDuration": 0.991796,
"DevToolsCommandDuration": 0.000323,
"ScriptDuration": 6.326745,
"V8CompileDuration": 0,
"TaskDuration": 11.079249,
"TaskOtherDuration": 2.831904,
"ThreadTime": 11.911973,
"ProcessTime": 14.42,
"JSHeapUsedSize": 8961292,
"JSHeapTotalSize": 12058624,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23282.130574,
"NavigationStart": 23279.964199
},
"snapshot": {
"elapsedMs": 20101.5,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 5
},
{
"key": "add:document:visibilitychange",
"count": 5
},
{
"key": "remove:window:online",
"count": 4
},
{
"key": "remove:document:visibilitychange",
"count": 4
},
{
"key": "add:audio:canplay",
"count": 3
},
{
"key": "add:audio:ended",
"count": 3
},
{
"key": "add:audio:error",
"count": 3
},
{
"key": "add:audio:pause",
"count": 3
},
{
"key": "add:audio:play",
"count": 3
},
{
"key": "add:audio:stalled",
"count": 3
},
{
"key": "add:audio:waiting",
"count": 3
},
{
"key": "add:div#root:auxclick",
"count": 2
},
{
"key": "add:div#root:click",
"count": 2
},
{
"key": "add:div#root:contextmenu",
"count": 2
},
{
"key": "add:div#root:copy",
"count": 2
},
{
"key": "add:div#root:cut",
"count": 2
},
{
"key": "add:div#root:drag",
"count": 2
},
{
"key": "add:div#root:dragend",
"count": 2
},
{
"key": "add:div#root:dragenter",
"count": 2
},
{
"key": "add:div#root:dragexit",
"count": 2
},
{
"key": "add:div#root:dragleave",
"count": 2
},
{
"key": "add:div#root:dragover",
"count": 2
},
{
"key": "add:div#root:dragstart",
"count": 2
},
{
"key": "add:div#root:drop",
"count": 2
},
{
"key": "add:div#root:gotpointercapture",
"count": 2
},
{
"key": "add:div#root:input",
"count": 2
},
{
"key": "add:div#root:keydown",
"count": 2
},
{
"key": "add:div#root:keypress",
"count": 2
},
{
"key": "add:div#root:keyup",
"count": 2
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
},
{
"key": "add:div#root:mousemove",
"count": 2
},
{
"key": "add:div#root:mouseout",
"count": 2
},
{
"key": "add:div#root:mouseover",
"count": 2
},
{
"key": "add:div#root:mouseup",
"count": 2
},
{
"key": "add:div#root:paste",
"count": 2
},
{
"key": "add:div#root:pointercancel",
"count": 2
},
{
"key": "add:div#root:pointerdown",
"count": 2
},
{
"key": "add:div#root:pointermove",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:12000",
"count": 2
},
{
"key": "interval:200",
"count": 2
},
{
"key": "timeout:15000",
"count": 1
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:4000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "interval:3000",
"count": 1
}
],
"timeouts": 10,
"intervals": 12,
"timeoutFires": 4,
"intervalFires": 45,
"webSocketMessageCount": 285,
"webSocketBytes": 5610397,
"webSocketEvents": [
{
"event": "log:entry",
"count": 150,
"bytes": 35714
},
{
"event": "sensorFrame",
"count": 76,
"bytes": 142196
},
{
"event": "session:sync",
"count": 18,
"bytes": 306294
},
{
"event": "commandAck",
"count": 12,
"bytes": 1128
},
{
"event": "",
"count": 4,
"bytes": 4992972
},
{
"event": "roverSnapshot:frame",
"count": 3,
"bytes": 291
},
{
"event": "0{\"sid\":\"CQG",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"-s",
"count": 1,
"bytes": 32
},
{
"event": "mode",
"count": 1,
"bytes": 27
},
{
"event": "controlGranted",
"count": 1,
"bytes": 41
},
{
"event": "alert",
"count": 1,
"bytes": 128
},
{
"event": "alert:new",
"count": 1,
"bytes": 165
},
{
"event": "auth:role",
"count": 1,
"bytes": 31
},
{
"event": "rovers",
"count": 1,
"bytes": 5453
},
{
"event": "chat:init",
"count": 1,
"bytes": 77613
},
{
"event": "overseer:memory",
"count": 1,
"bytes": 520
},
{
"event": "log:init",
"count": 1,
"bytes": 44230
},
{
"event": "barcode:state",
"count": 1,
"bytes": 1627
},
{
"event": "kinect:pointCloudFrame",
"count": 1,
"bytes": 262
},
{
"event": "kinect:colorFrame",
"count": 1,
"bytes": 169
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "431[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 766
},
{
"event": "2",
"count": 1,
"bytes": 1
},
{
"event": "433[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "roverSnapshot:status",
"count": 1,
"bytes": 79
},
{
"event": "WALL-E",
"count": 1,
"bytes": 40
}
],
"mutationsTotal": {
"records": 4729,
"added": 175,
"removed": 11,
"attrs": 4524,
"text": 19
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 1866
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 933
},
{
"key": "div.h-full:attributes:style",
"count": 580
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 292
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 290
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 242
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 121
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 28
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 28
},
{
"key": "Text:characterData:",
"count": 19
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 18
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 18
},
{
"key": "input:attributes:name",
"count": 14
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 14
},
{
"key": "section.panel-section.border:attributes:style",
"count": 10
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 10
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 9
},
{
"key": "header.flex.items-center:attributes:style",
"count": 9
},
{
"key": "input:attributes:type",
"count": 7
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 6
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 2
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 1
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 1
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 1
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
},
{
"key": "audio:attributes:autoplay",
"count": 1
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 1
}
],
"mutationBursts": [
{
"at": 7974,
"records": 241,
"added": 123,
"removed": 6,
"attrs": 110,
"text": 2
},
{
"at": 11575,
"records": 147,
"added": 11,
"removed": 2,
"attrs": 132,
"text": 2
},
{
"at": 12385,
"records": 131,
"added": 0,
"removed": 0,
"attrs": 127,
"text": 4
},
{
"at": 12981,
"records": 119,
"added": 0,
"removed": 0,
"attrs": 119,
"text": 0
},
{
"at": 18332,
"records": 117,
"added": 0,
"removed": 0,
"attrs": 116,
"text": 1
},
{
"at": 17140,
"records": 111,
"added": 0,
"removed": 0,
"attrs": 111,
"text": 0
},
{
"at": 13371,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 13636,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 13804,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 14271,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15643,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15909,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 18503,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 15238,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15283,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15415,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15495,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 15947,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 16169,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 16317,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
}
],
"frames": {
"count": 86,
"average": 206.1918604651163,
"p95": 566.5999999999995,
"p99": 3399.8999999999996
},
"longTasks": {
"count": 61,
"top": [
{
"startTime": 5290.5,
"duration": 2689,
"name": "self"
},
{
"startTime": 365.4000000022352,
"duration": 1807,
"name": "self"
},
{
"startTime": 2182.900000002235,
"duration": 1038,
"name": "self"
},
{
"startTime": 10371.10000000149,
"duration": 709,
"name": "self"
},
{
"startTime": 9852,
"duration": 519,
"name": "self"
},
{
"startTime": 8202.60000000149,
"duration": 394,
"name": "self"
},
{
"startTime": 3221.699999999255,
"duration": 385,
"name": "self"
},
{
"startTime": 3882.5,
"duration": 356,
"name": "self"
},
{
"startTime": 11221.5,
"duration": 353,
"name": "self"
},
{
"startTime": 18042.900000002235,
"duration": 292,
"name": "self"
},
{
"startTime": 12110.900000002235,
"duration": 276,
"name": "self"
},
{
"startTime": 12738.800000000745,
"duration": 243,
"name": "self"
},
{
"startTime": 16946.5,
"duration": 195,
"name": "self"
},
{
"startTime": 11575.5,
"duration": 186,
"name": "self"
},
{
"startTime": 3615.60000000149,
"duration": 185,
"name": "self"
},
{
"startTime": 14671.699999999255,
"duration": 160,
"name": "self"
},
{
"startTime": 4239.199999999255,
"duration": 130,
"name": "self"
},
{
"startTime": 14837.5,
"duration": 110,
"name": "self"
},
{
"startTime": 15717.60000000149,
"duration": 108,
"name": "self"
},
{
"startTime": 17352.400000002235,
"duration": 107,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

@@ -0,0 +1,900 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-09-03-973Z-root-runtime",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 23379.874681,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 683,
"LayoutObjects": 1975,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2710,
"Resources": 20,
"ContextLifecycleStateObservers": 20,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 4,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 12,
"LayoutCount": 49,
"RecalcStyleCount": 77,
"LayoutDuration": 1.49198,
"RecalcStyleDuration": 1.886372,
"DevToolsCommandDuration": 0.000349,
"ScriptDuration": 14.419283,
"V8CompileDuration": 0,
"TaskDuration": 29.584723,
"TaskOtherDuration": 11.786739,
"ThreadTime": 32.430498,
"ProcessTime": 38.63,
"JSHeapUsedSize": 7496328,
"JSHeapTotalSize": 13107200,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23349.78059,
"NavigationStart": 23346.788167
},
"snapshot": {
"elapsedMs": 38566.699999999255,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 6
},
{
"key": "add:document:visibilitychange",
"count": 6
},
{
"key": "add:audio:canplay",
"count": 6
},
{
"key": "add:audio:ended",
"count": 6
},
{
"key": "add:audio:error",
"count": 6
},
{
"key": "add:audio:pause",
"count": 6
},
{
"key": "add:audio:play",
"count": 6
},
{
"key": "add:audio:stalled",
"count": 6
},
{
"key": "add:audio:waiting",
"count": 6
},
{
"key": "remove:window:online",
"count": 5
},
{
"key": "remove:document:visibilitychange",
"count": 5
},
{
"key": "remove:audio:play",
"count": 4
},
{
"key": "remove:audio:pause",
"count": 4
},
{
"key": "remove:audio:ended",
"count": 4
},
{
"key": "remove:audio:error",
"count": 4
},
{
"key": "remove:audio:waiting",
"count": 4
},
{
"key": "remove:audio:canplay",
"count": 4
},
{
"key": "remove:audio:stalled",
"count": 4
},
{
"key": "add:WebSocket:message",
"count": 2
},
{
"key": "add:div#root:auxclick",
"count": 2
},
{
"key": "add:div#root:click",
"count": 2
},
{
"key": "add:div#root:contextmenu",
"count": 2
},
{
"key": "add:div#root:copy",
"count": 2
},
{
"key": "add:div#root:cut",
"count": 2
},
{
"key": "add:div#root:drag",
"count": 2
},
{
"key": "add:div#root:dragend",
"count": 2
},
{
"key": "add:div#root:dragenter",
"count": 2
},
{
"key": "add:div#root:dragexit",
"count": 2
},
{
"key": "add:div#root:dragleave",
"count": 2
},
{
"key": "add:div#root:dragover",
"count": 2
},
{
"key": "add:div#root:dragstart",
"count": 2
},
{
"key": "add:div#root:drop",
"count": 2
},
{
"key": "add:div#root:gotpointercapture",
"count": 2
},
{
"key": "add:div#root:input",
"count": 2
},
{
"key": "add:div#root:keydown",
"count": 2
},
{
"key": "add:div#root:keypress",
"count": 2
},
{
"key": "add:div#root:keyup",
"count": 2
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:200",
"count": 4
},
{
"key": "timeout:2000",
"count": 4
},
{
"key": "timeout:12000",
"count": 3
},
{
"key": "interval:60000",
"count": 3
},
{
"key": "timeout:15000",
"count": 2
},
{
"key": "timeout:4000",
"count": 2
},
{
"key": "interval:3000",
"count": 2
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "timeout:1282",
"count": 1
},
{
"key": "timeout:1257",
"count": 1
}
],
"timeouts": 19,
"intervals": 15,
"timeoutFires": 10,
"intervalFires": 121,
"webSocketMessageCount": 533,
"webSocketBytes": 11199188,
"webSocketEvents": [
{
"event": "log:entry",
"count": 308,
"bytes": 71889
},
{
"event": "sensorFrame",
"count": 90,
"bytes": 168271
},
{
"event": "commandAck",
"count": 36,
"bytes": 3396
},
{
"event": "session:sync",
"count": 33,
"bytes": 560916
},
{
"event": "",
"count": 14,
"bytes": 10130394
},
{
"event": "roverSnapshot:frame",
"count": 10,
"bytes": 967
},
{
"event": "mode",
"count": 2,
"bytes": 54
},
{
"event": "controlGranted",
"count": 2,
"bytes": 82
},
{
"event": "alert",
"count": 2,
"bytes": 256
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "auth:role",
"count": 2,
"bytes": 62
},
{
"event": "rovers",
"count": 2,
"bytes": 10906
},
{
"event": "chat:init",
"count": 2,
"bytes": 155226
},
{
"event": "overseer:memory",
"count": 2,
"bytes": 1040
},
{
"event": "log:init",
"count": 2,
"bytes": 89130
},
{
"event": "barcode:state",
"count": 2,
"bytes": 3254
},
{
"event": "kinect:pointCloudFrame",
"count": 2,
"bytes": 524
},
{
"event": "kinect:colorFrame",
"count": 2,
"bytes": 338
},
{
"event": "roverSnapshot:status",
"count": 2,
"bytes": 156
},
{
"event": "0{\"sid\":\"a3i",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"Bp",
"count": 1,
"bytes": 32
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 40
},
{
"event": "431[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "2",
"count": 1,
"bytes": 1
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "433[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "WALL-E",
"count": 1,
"bytes": 40
},
{
"event": "0{\"sid\":\"3kq",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"BX",
"count": 1,
"bytes": 32
},
{
"event": "436[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "437[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "438[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "439[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "Freaky",
"count": 1,
"bytes": 41
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 767
}
],
"mutationsTotal": {
"records": 6037,
"added": 199,
"removed": 35,
"attrs": 5767,
"text": 36
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 2648
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 1324
},
{
"key": "div.h-full:attributes:style",
"count": 512
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 338
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 262
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 256
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 169
},
{
"key": "Text:characterData:",
"count": 36
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 32
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 28
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 22
},
{
"key": "section.panel-section.border:attributes:style",
"count": 20
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 18
},
{
"key": "header.flex.items-center:attributes:style",
"count": 18
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 18
},
{
"key": "input:attributes:name",
"count": 14
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 14
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 11
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 10
},
{
"key": "input:attributes:type",
"count": 7
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 4
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 4
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 3
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "audio:attributes:autoplay",
"count": 2
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "span.inline-block.rounded:attributes:style",
"count": 2
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
}
],
"mutationBursts": [
{
"at": 10936,
"records": 241,
"added": 123,
"removed": 6,
"attrs": 110,
"text": 2
},
{
"at": 27789,
"records": 177,
"added": 9,
"removed": 17,
"attrs": 145,
"text": 6
},
{
"at": 17412,
"records": 133,
"added": 2,
"removed": 2,
"attrs": 118,
"text": 11
},
{
"at": 31614,
"records": 111,
"added": 2,
"removed": 1,
"attrs": 104,
"text": 4
},
{
"at": 37740,
"records": 100,
"added": 0,
"removed": 1,
"attrs": 99,
"text": 0
},
{
"at": 38360,
"records": 99,
"added": 0,
"removed": 0,
"attrs": 99,
"text": 0
},
{
"at": 17915,
"records": 59,
"added": 11,
"removed": 2,
"attrs": 45,
"text": 1
},
{
"at": 19317,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 20163,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 20363,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 23753,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 24543,
"records": 51,
"added": 0,
"removed": 0,
"attrs": 51,
"text": 0
},
{
"at": 31831,
"records": 51,
"added": 11,
"removed": 2,
"attrs": 37,
"text": 1
},
{
"at": 18579,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 18763,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 19129,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 19374,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 21234,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 22011,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
},
{
"at": 22767,
"records": 47,
"added": 0,
"removed": 0,
"attrs": 47,
"text": 0
}
],
"frames": {
"count": 181,
"average": 185.53591160220995,
"p95": 349.90000000000146,
"p99": 1849.9000000000015
},
"longTasks": {
"count": 165,
"top": [
{
"startTime": 7151.60000000149,
"duration": 3795,
"name": "self"
},
{
"startTime": 459.6000000014901,
"duration": 2544,
"name": "self"
},
{
"startTime": 3019.199999999255,
"duration": 1426,
"name": "self"
},
{
"startTime": 29673.900000002235,
"duration": 1066,
"name": "self"
},
{
"startTime": 14211,
"duration": 972,
"name": "self"
},
{
"startTime": 28901.800000000745,
"duration": 772,
"name": "self"
},
{
"startTime": 13490.699999999255,
"duration": 720,
"name": "self"
},
{
"startTime": 16782.400000002235,
"duration": 639,
"name": "self"
},
{
"startTime": 11240.199999999255,
"duration": 552,
"name": "self"
},
{
"startTime": 31092.5,
"duration": 524,
"name": "self"
},
{
"startTime": 4445.60000000149,
"duration": 495,
"name": "self"
},
{
"startTime": 5394.800000000745,
"duration": 459,
"name": "self"
},
{
"startTime": 27348.199999999255,
"duration": 445,
"name": "self"
},
{
"startTime": 24835.10000000149,
"duration": 363,
"name": "self"
},
{
"startTime": 4963.800000000745,
"duration": 272,
"name": "self"
},
{
"startTime": 37513.60000000149,
"duration": 228,
"name": "self"
},
{
"startTime": 17919.5,
"duration": 215,
"name": "self"
},
{
"startTime": 27801.60000000149,
"duration": 193,
"name": "self"
},
{
"startTime": 17425.10000000149,
"duration": 179,
"name": "self"
},
{
"startTime": 31885.900000002235,
"duration": 176,
"name": "self"
}
]
},
"consoleErrors": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

@@ -0,0 +1,529 @@
{
"url": "http://127.0.0.1:4173/",
"runDir": "perf/results/2026-06-12T06-11-56-994Z",
"elapsedMs": 83940,
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.7778.96 Safari/537.36",
"title": "Roomba Rover",
"finalUrl": "http://127.0.0.1:4173/",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true
},
"metrics": {
"afterLoad": {
"Timestamp": 23527.948042,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 267,
"Nodes": 651,
"LayoutCount": 5,
"RecalcStyleCount": 7,
"LayoutDuration": 0.500736,
"RecalcStyleDuration": 0.283234,
"ScriptDuration": 5.693685,
"TaskDuration": 7.662781,
"JSHeapUsedSize": 4764568,
"JSHeapTotalSize": 7077888
},
"end": {
"Timestamp": 23593.417486,
"Documents": 1,
"Frames": 1,
"JSEventListeners": 1032,
"Nodes": 2696,
"LayoutCount": 137,
"RecalcStyleCount": 280,
"LayoutDuration": 3.566888,
"RecalcStyleDuration": 3.703827,
"ScriptDuration": 27.965461,
"TaskDuration": 67.992237,
"JSHeapUsedSize": 12832824,
"JSHeapTotalSize": 19759104
},
"delta": {
"Timestamp": 65.46944399999848,
"Documents": -2,
"Frames": 0,
"JSEventListeners": 765,
"Nodes": 2045,
"LayoutCount": 132,
"RecalcStyleCount": 273,
"LayoutDuration": 3.066152,
"RecalcStyleDuration": 3.420593,
"ScriptDuration": 22.271776000000003,
"TaskDuration": 60.329456,
"JSHeapUsedSize": 8068256,
"JSHeapTotalSize": 12681216
}
},
"cpuSummary": {
"totalProfileMs": 55106.451,
"sampleCount": 40511,
"topSelfTime": [
{
"url": "",
"functionName": "(program)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 26825.782999999923,
"samples": 18512
},
{
"url": "",
"functionName": "(idle)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 4815.981000000005,
"samples": 3701
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "(anonymous)",
"lineNumber": 11,
"columnNumber": 40267,
"selfMs": 2656.4819999999972,
"samples": 2026
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ad",
"lineNumber": 9,
"columnNumber": 7898,
"selfMs": 1032.9479999999999,
"samples": 818
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Kt",
"lineNumber": 8,
"columnNumber": 48104,
"selfMs": 951.7199999999993,
"samples": 747
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Ce",
"lineNumber": 8,
"columnNumber": 94317,
"selfMs": 905.2510000000005,
"samples": 727
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "tryParse",
"lineNumber": 11,
"columnNumber": 49539,
"selfMs": 592.3200000000003,
"samples": 480
},
{
"url": "",
"functionName": "(garbage collector)",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 578.5280000000001,
"samples": 430
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "start",
"lineNumber": 11,
"columnNumber": 1485400,
"selfMs": 445.5410000000001,
"samples": 322
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "G4",
"lineNumber": 8,
"columnNumber": 28256,
"selfMs": 432.7939999999998,
"samples": 325
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "lt",
"lineNumber": 8,
"columnNumber": 7348,
"selfMs": 360.4799999999999,
"samples": 279
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ga",
"lineNumber": 8,
"columnNumber": 10651,
"selfMs": 330.2019999999997,
"samples": 260
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "r",
"lineNumber": 1,
"columnNumber": 967,
"selfMs": 323.32300000000004,
"samples": 239
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Rl",
"lineNumber": 8,
"columnNumber": 101212,
"selfMs": 308.80500000000006,
"samples": 225
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "S1",
"lineNumber": 8,
"columnNumber": 40721,
"selfMs": 305.08099999999996,
"samples": 243
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "C2",
"lineNumber": 8,
"columnNumber": 49311,
"selfMs": 304.76900000000006,
"samples": 250
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "ws.onmessage",
"lineNumber": 11,
"columnNumber": 33453,
"selfMs": 284.5850000000001,
"samples": 204
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "N2.emit",
"lineNumber": 11,
"columnNumber": 25371,
"selfMs": 275.96699999999987,
"samples": 219
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Xl",
"lineNumber": 8,
"columnNumber": 117224,
"selfMs": 260.1920000000001,
"samples": 208
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "pa",
"lineNumber": 8,
"columnNumber": 10399,
"selfMs": 249.32600000000016,
"samples": 180
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "S4",
"lineNumber": 11,
"columnNumber": 91365,
"selfMs": 237.0250000000001,
"samples": 179
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "N6",
"lineNumber": 8,
"columnNumber": 34144,
"selfMs": 234.33099999999988,
"samples": 188
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "bl",
"lineNumber": 8,
"columnNumber": 80838,
"selfMs": 228.66600000000017,
"samples": 171
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "o2",
"lineNumber": 8,
"columnNumber": 42232,
"selfMs": 219.2690000000001,
"samples": 182
},
{
"url": "",
"functionName": "setAttribute",
"lineNumber": -1,
"columnNumber": -1,
"selfMs": 213.44599999999997,
"samples": 178
},
{
"url": "",
"functionName": "(anonymous)",
"lineNumber": 29,
"columnNumber": 16,
"selfMs": 194.01300000000003,
"samples": 148
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "s2",
"lineNumber": 9,
"columnNumber": 71,
"selfMs": 189.78200000000004,
"samples": 160
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "na",
"lineNumber": 8,
"columnNumber": 3730,
"selfMs": 184.6409999999999,
"samples": 146
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Eh",
"lineNumber": 8,
"columnNumber": 95737,
"selfMs": 179.85799999999998,
"samples": 138
},
{
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"functionName": "Oh",
"lineNumber": 8,
"columnNumber": 116259,
"selfMs": 179.09700000000007,
"samples": 134
}
]
},
"responsiveness": {
"longTaskCount": 333,
"topLongTasks": [
{
"name": "self",
"startTime": 7254.699999999255,
"duration": 3731
},
{
"name": "self",
"startTime": 409.19999999925494,
"duration": 2518
},
{
"name": "unknown",
"startTime": 13437.400000002235,
"duration": 1801
},
{
"name": "self",
"startTime": 2960.199999999255,
"duration": 1382
},
{
"name": "self",
"startTime": 24715.300000000745,
"duration": 1038
},
{
"name": "self",
"startTime": 54470.699999999255,
"duration": 1002
},
{
"name": "self",
"startTime": 53685.199999999255,
"duration": 785
},
{
"name": "self",
"startTime": 23959.699999999255,
"duration": 755
},
{
"name": "self",
"startTime": 50997,
"duration": 747
},
{
"name": "self",
"startTime": 4343.10000000149,
"duration": 621
},
{
"name": "self",
"startTime": 27087.5,
"duration": 597
},
{
"name": "self",
"startTime": 22357.300000000745,
"duration": 554
},
{
"name": "unknown",
"startTime": 15496,
"duration": 478
},
{
"name": "self",
"startTime": 5324.5,
"duration": 474
},
{
"name": "self",
"startTime": 11302.300000000745,
"duration": 448
},
{
"name": "self",
"startTime": 40393.300000000745,
"duration": 435
},
{
"name": "self",
"startTime": 55742.5,
"duration": 405
},
{
"name": "self",
"startTime": 25906.300000000745,
"duration": 323
},
{
"name": "self",
"startTime": 38613.5,
"duration": 303
},
{
"name": "self",
"startTime": 46738.5,
"duration": 286
}
],
"frameCount": 358,
"averageFrameMs": 207.5804469273743,
"p95FrameMs": 483.3000000000029,
"p99FrameMs": 2016.6,
"intervalDriftCount": 43,
"averageIntervalDriftMs": 1537.434883720913,
"p95IntervalDriftMs": 7938.5,
"p99IntervalDriftMs": 8055.300000000745
},
"consoleMessages": [
{
"type": "info",
"text": "[socket] connecting to https://rover.otter.land",
"location": {
"url": "http://127.0.0.1:4173/assets/index-BcKZ4JVW.js",
"line": 11,
"column": 63824,
"lineNumber": 11,
"columnNumber": 63824
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 404 ()",
"location": {
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
},
{
"type": "error",
"text": "Failed to load resource: the server responded with a status of 400 ()",
"location": {
"url": "https://rover.otter.land/video/Bweeble/whep",
"line": 0,
"column": 0,
"lineNumber": 0,
"columnNumber": 0
}
}
],
"failedRequests": [
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
},
{
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"method": "POST",
"failure": "net::ERR_ABORTED"
}
],
"errorResponses": [
{
"url": "https://media.discordapp.net/attachments/1424887548622405672/1512626709692682340/cat2.PNG?ex=6a24c6e6&is=6a237566&hm=baeec4c8e666888d89711065b1dbcb391c3415f9c28d1917bdd286776b60c867&=&format=webp&quality=lossless",
"status": 404
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble-audio/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"status": 400
},
{
"url": "https://rover.otter.land/video/Bweeble/whep",
"status": 400
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,896 @@
{
"url": "https://rover.otter.land/",
"runDir": "perf/results/2026-06-12T06-15-19-037Z-root-runtime",
"environment": {
"cpuThrottle": 8,
"viewport": {
"width": 390,
"height": 844
},
"isMobile": true,
"sampleMs": 30000
},
"metrics": {
"Timestamp": 23755.664379,
"AudioHandlers": 0,
"AudioWorkletProcessors": 0,
"Documents": 3,
"Frames": 1,
"JSEventListeners": 1280,
"LayoutObjects": 1996,
"MediaKeySessions": 0,
"MediaKeys": 0,
"Nodes": 2628,
"Resources": 20,
"ContextLifecycleStateObservers": 20,
"V8PerContextDatas": 4,
"WorkerGlobalScopes": 0,
"UACSSResources": 0,
"RTCPeerConnections": 4,
"ResourceFetchers": 3,
"AdSubframes": 0,
"DetachedScriptStates": 2,
"ArrayBufferContents": 14,
"LayoutCount": 43,
"RecalcStyleCount": 80,
"LayoutDuration": 1.465119,
"RecalcStyleDuration": 1.54029,
"DevToolsCommandDuration": 0.00038,
"ScriptDuration": 15.671196,
"V8CompileDuration": 0,
"TaskDuration": 30.021546,
"TaskOtherDuration": 11.344561,
"ThreadTime": 32.951976,
"ProcessTime": 39.43,
"JSHeapUsedSize": 7054604,
"JSHeapTotalSize": 13893632,
"FirstMeaningfulPaint": 0,
"DomContentLoaded": 23725.629234,
"NavigationStart": 23721.84567
},
"snapshot": {
"elapsedMs": 53572.199999999255,
"listeners": [
{
"key": "add:input:invalid",
"count": 18
},
{
"key": "add:window:online",
"count": 6
},
{
"key": "add:document:visibilitychange",
"count": 6
},
{
"key": "add:audio:canplay",
"count": 6
},
{
"key": "add:audio:ended",
"count": 6
},
{
"key": "add:audio:error",
"count": 6
},
{
"key": "add:audio:pause",
"count": 6
},
{
"key": "add:audio:play",
"count": 6
},
{
"key": "add:audio:stalled",
"count": 6
},
{
"key": "add:audio:waiting",
"count": 6
},
{
"key": "remove:window:online",
"count": 5
},
{
"key": "remove:document:visibilitychange",
"count": 5
},
{
"key": "remove:audio:play",
"count": 4
},
{
"key": "remove:audio:pause",
"count": 4
},
{
"key": "remove:audio:ended",
"count": 4
},
{
"key": "remove:audio:error",
"count": 4
},
{
"key": "remove:audio:waiting",
"count": 4
},
{
"key": "remove:audio:canplay",
"count": 4
},
{
"key": "remove:audio:stalled",
"count": 4
},
{
"key": "add:WebSocket:message",
"count": 2
},
{
"key": "add:div#root:auxclick",
"count": 2
},
{
"key": "add:div#root:click",
"count": 2
},
{
"key": "add:div#root:contextmenu",
"count": 2
},
{
"key": "add:div#root:copy",
"count": 2
},
{
"key": "add:div#root:cut",
"count": 2
},
{
"key": "add:div#root:drag",
"count": 2
},
{
"key": "add:div#root:dragend",
"count": 2
},
{
"key": "add:div#root:dragenter",
"count": 2
},
{
"key": "add:div#root:dragexit",
"count": 2
},
{
"key": "add:div#root:dragleave",
"count": 2
},
{
"key": "add:div#root:dragover",
"count": 2
},
{
"key": "add:div#root:dragstart",
"count": 2
},
{
"key": "add:div#root:drop",
"count": 2
},
{
"key": "add:div#root:gotpointercapture",
"count": 2
},
{
"key": "add:div#root:input",
"count": 2
},
{
"key": "add:div#root:keydown",
"count": 2
},
{
"key": "add:div#root:keypress",
"count": 2
},
{
"key": "add:div#root:keyup",
"count": 2
},
{
"key": "add:div#root:lostpointercapture",
"count": 2
},
{
"key": "add:div#root:mousedown",
"count": 2
}
],
"timers": [
{
"key": "timeout:0",
"count": 5
},
{
"key": "timeout:12000",
"count": 4
},
{
"key": "interval:60000",
"count": 4
},
{
"key": "interval:250",
"count": 4
},
{
"key": "interval:200",
"count": 4
},
{
"key": "timeout:2000",
"count": 4
},
{
"key": "timeout:15000",
"count": 2
},
{
"key": "timeout:4000",
"count": 2
},
{
"key": "interval:3000",
"count": 2
},
{
"key": "interval:1000",
"count": 1
},
{
"key": "interval:2000",
"count": 1
},
{
"key": "timeout:10000",
"count": 1
},
{
"key": "timeout:1491",
"count": 1
}
],
"timeouts": 19,
"intervals": 16,
"timeoutFires": 11,
"intervalFires": 136,
"webSocketMessageCount": 498,
"webSocketBytes": 11209457,
"webSocketEvents": [
{
"event": "log:entry",
"count": 269,
"bytes": 62887
},
{
"event": "sensorFrame",
"count": 99,
"bytes": 185012
},
{
"event": "session:sync",
"count": 31,
"bytes": 526178
},
{
"event": "commandAck",
"count": 30,
"bytes": 2833
},
{
"event": "",
"count": 15,
"bytes": 10168579
},
{
"event": "roverSnapshot:frame",
"count": 11,
"bytes": 1067
},
{
"event": "mode",
"count": 2,
"bytes": 54
},
{
"event": "controlGranted",
"count": 2,
"bytes": 83
},
{
"event": "alert",
"count": 2,
"bytes": 257
},
{
"event": "alert:new",
"count": 2,
"bytes": 330
},
{
"event": "auth:role",
"count": 2,
"bytes": 62
},
{
"event": "rovers",
"count": 2,
"bytes": 10906
},
{
"event": "chat:init",
"count": 2,
"bytes": 155226
},
{
"event": "overseer:memory",
"count": 2,
"bytes": 1040
},
{
"event": "log:init",
"count": 2,
"bytes": 88662
},
{
"event": "barcode:state",
"count": 2,
"bytes": 3254
},
{
"event": "kinect:pointCloudFrame",
"count": 2,
"bytes": 524
},
{
"event": "kinect:colorFrame",
"count": 2,
"bytes": 338
},
{
"event": "2",
"count": 2,
"bytes": 2
},
{
"event": "roverSnapshot:status",
"count": 2,
"bytes": 159
},
{
"event": "0{\"sid\":\"v0f",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"5t",
"count": 1,
"bytes": 32
},
{
"event": "430[{\"succes",
"count": 1,
"bytes": 39
},
{
"event": "431[{\"url\":\"",
"count": 1,
"bytes": 133
},
{
"event": "432[{\"url\":\"",
"count": 1,
"bytes": 145
},
{
"event": "Freaky",
"count": 1,
"bytes": 40
},
{
"event": "434[{\"succes",
"count": 1,
"bytes": 194
},
{
"event": "0{\"sid\":\"t1y",
"count": 1,
"bytes": 106
},
{
"event": "40{\"sid\":\"Bu",
"count": 1,
"bytes": 32
},
{
"event": "436[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "437[{\"error\"",
"count": 1,
"bytes": 41
},
{
"event": "438[{\"url\":\"",
"count": 1,
"bytes": 135
},
{
"event": "439[{\"url\":\"",
"count": 1,
"bytes": 147
},
{
"event": "Bweeble",
"count": 1,
"bytes": 42
},
{
"event": "roverHostStats",
"count": 1,
"bytes": 771
}
],
"mutationsTotal": {
"records": 7648,
"added": 227,
"removed": 34,
"attrs": 7351,
"text": 36
},
"mutationTargets": [
{
"key": "input.accent-emerald-400:attributes:name",
"count": 2612
},
{
"key": "input.accent-emerald-400:attributes:type",
"count": 1306
},
{
"key": "input.field-input.flex-1:attributes:name",
"count": 432
},
{
"key": "input.field-input.w-full:attributes:name",
"count": 432
},
{
"key": "input.accent-cyan-500:attributes:name",
"count": 428
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:name",
"count": 330
},
{
"key": "div.h-full:attributes:style",
"count": 278
},
{
"key": "input.field-input.chat-composer-input:attributes:name",
"count": 216
},
{
"key": "input:attributes:name",
"count": 216
},
{
"key": "input.field-input.w-full:attributes:type",
"count": 216
},
{
"key": "input.accent-cyan-500:attributes:type",
"count": 214
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:type",
"count": 165
},
{
"key": "path.[object.SVGAnimatedString]:attributes:d",
"count": 145
},
{
"key": "path.[object.SVGAnimatedString]:attributes:fill",
"count": 139
},
{
"key": "input:attributes:type",
"count": 108
},
{
"key": "Text:characterData:",
"count": 36
},
{
"key": "section.panel-section.border:attributes:style",
"count": 20
},
{
"key": "header.flex.items-center:attributes:style",
"count": 18
},
{
"key": "path.[object.SVGAnimatedString]:attributes:stroke",
"count": 18
},
{
"key": "input.min-w-0.flex-1:attributes:name",
"count": 14
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:value",
"count": 3
},
{
"key": "span.inline-flex.items-center:attributes:class",
"count": 3
},
{
"key": "div.rounded-sm.px-2:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:disabled",
"count": 3
},
{
"key": "button.mobile-touch-control.min-h-9:attributes:disabled",
"count": 3
},
{
"key": "div.fixed.bottom-2:attributes:class",
"count": 3
},
{
"key": "button.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "div.mobile-touch-control.flex:attributes:class",
"count": 3
},
{
"key": "input#replay-sources-mobile-portrait-title.field-input.min-w-0:attributes:placeholder",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:playsinline",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:autoplay",
"count": 2
},
{
"key": "video.h-full.w-full:attributes:disableremoteplayback",
"count": 2
},
{
"key": "audio:attributes:autoplay",
"count": 2
},
{
"key": "audio:attributes:disableremoteplayback",
"count": 2
},
{
"key": "input.field-input.flex-1:attributes:value",
"count": 2
},
{
"key": "span.inline-block.rounded:attributes:style",
"count": 2
},
{
"key": "div.flex.flex-col:attributes:class",
"count": 1
},
{
"key": "button.button-dark.min-w-0:attributes:disabled",
"count": 1
},
{
"key": "input.field-input.w-full:attributes:value",
"count": 1
},
{
"key": "button.button-dark.text-sm:attributes:disabled",
"count": 1
}
],
"mutationBursts": [
{
"at": 10155,
"records": 164,
"added": 105,
"removed": 1,
"attrs": 58,
"text": 0
},
{
"at": 28081,
"records": 103,
"added": 7,
"removed": 12,
"attrs": 79,
"text": 5
},
{
"at": 31972,
"records": 83,
"added": 13,
"removed": 3,
"attrs": 65,
"text": 2
},
{
"at": 18426,
"records": 82,
"added": 11,
"removed": 2,
"attrs": 66,
"text": 3
},
{
"at": 8027,
"records": 69,
"added": 19,
"removed": 5,
"attrs": 42,
"text": 3
},
{
"at": 16714,
"records": 66,
"added": 2,
"removed": 2,
"attrs": 53,
"text": 9
},
{
"at": 40645,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 40951,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 42708,
"records": 64,
"added": 0,
"removed": 0,
"attrs": 64,
"text": 0
},
{
"at": 36974,
"records": 61,
"added": 0,
"removed": 0,
"attrs": 61,
"text": 0
},
{
"at": 24109,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 28572,
"records": 60,
"added": 2,
"removed": 4,
"attrs": 53,
"text": 1
},
{
"at": 36595,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 42135,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 43257,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 48207,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 48500,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 50368,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 51568,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
},
{
"at": 52146,
"records": 60,
"added": 0,
"removed": 0,
"attrs": 60,
"text": 0
}
],
"frames": {
"count": 241,
"average": 202.06639004149378,
"p95": 399.8999999999978,
"p99": 1633.199999999999
},
"longTasks": {
"count": 133,
"top": [
{
"startTime": 1510.7999999970198,
"duration": 2281,
"name": "self"
},
{
"startTime": 10156.89999999851,
"duration": 1493,
"name": "self"
},
{
"startTime": 3806,
"duration": 1311,
"name": "self"
},
{
"startTime": 30277.099999997765,
"duration": 932,
"name": "self"
},
{
"startTime": 14996,
"duration": 931,
"name": "self"
},
{
"startTime": 9269.89999999851,
"duration": 886,
"name": "self"
},
{
"startTime": 7147,
"duration": 880,
"name": "self"
},
{
"startTime": 14298,
"duration": 698,
"name": "self"
},
{
"startTime": 29581.099999997765,
"duration": 696,
"name": "self"
},
{
"startTime": 8029.599999997765,
"duration": 689,
"name": "self"
},
{
"startTime": 5117.39999999851,
"duration": 534,
"name": "self"
},
{
"startTime": 31587,
"duration": 385,
"name": "self"
},
{
"startTime": 18074,
"duration": 356,
"name": "self"
},
{
"startTime": 25434.39999999851,
"duration": 344,
"name": "self"
},
{
"startTime": 16382.29999999702,
"duration": 333,
"name": "self"
},
{
"startTime": 12463.199999999255,
"duration": 300,
"name": "self"
},
{
"startTime": 6124.79999999702,
"duration": 285,
"name": "self"
},
{
"startTime": 5657.199999999255,
"duration": 283,
"name": "self"
},
{
"startTime": 18655.79999999702,
"duration": 242,
"name": "self"
},
{
"startTime": 19023.5,
"duration": 236,
"name": "self"
}
]
},
"consoleErrors": []
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -12,7 +12,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DhJId5qi.js"></script> <script type="module" crossorigin src="/assets/index-D0FgC_A-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css"> <link rel="stylesheet" crossorigin href="/assets/index-BVKa8Zph.css">
</head> </head>
<body> <body>
+2 -3
View File
@@ -11,7 +11,7 @@ import {
ControlSystemProvider, ControlSystemProvider,
KeyboardInputManager, KeyboardInputManager,
GamepadInputManager, GamepadInputManager,
useControlSystem, useControlSelector,
} from './controls/index.js'; } from './controls/index.js';
import RoomCameraPanel from './components/RoomCameraPanel/index.jsx'; import RoomCameraPanel from './components/RoomCameraPanel/index.jsx';
import KinectPanel from './components/KinectPanel/index.jsx'; import KinectPanel from './components/KinectPanel/index.jsx';
@@ -105,10 +105,9 @@ function MobileFeatureTabs({
const roverId = String(state.session?.assignment?.roverId || '').trim(); const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null; return roverId ? state.session?.audioForward?.[roverId] || null : null;
}); });
const { state: controlState } = useControlSystem(); const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400'; const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled); const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live'; const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean( const vipMicActive = Boolean(
@@ -2,7 +2,7 @@
// Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file. // Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js'; import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
@@ -103,10 +103,9 @@ export default function DriveDockAction({
compactHeightClass = '', compactHeightClass = '',
}) { }) {
const isMobile = layout === 'mobile'; const isMobile = layout === 'mobile';
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId, keymap }, const keymap = useControlSelector((control) => control.state.keymap);
actions, const actions = useControlActions();
} = useControlSystem();
const dockAssist = useManualDockAssist(); const dockAssist = useManualDockAssist();
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const state = driveDockState ?? deriveDriveDockState(frame); const state = driveDockState ?? deriveDriveDockState(frame);
+2 -4
View File
@@ -1,6 +1,6 @@
import { useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx'; import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx';
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import { useDriverVideoModePolicy } from '../../hooks/useDriverVideoModePolicy.js'; import { useDriverVideoModePolicy } from '../../hooks/useDriverVideoModePolicy.js';
import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx'; import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx';
import HudOverlay from '../HudOverlays/HudOverlay/index.jsx'; import HudOverlay from '../HudOverlays/HudOverlay/index.jsx';
@@ -15,9 +15,7 @@ import CardFrame from '../CardFrame/index.jsx';
export default function DriverVideo({ layoutFormat = 'desktop' }) { export default function DriverVideo({ layoutFormat = 'desktop' }) {
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const videoMode = useDriverVideoModePolicy(roverId); const videoMode = useDriverVideoModePolicy(roverId);
const { const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
state: { lastControlIntentAt },
} = useControlSystem();
if (!roverId) { if (!roverId) {
return ( return (
+4 -4
View File
@@ -1,13 +1,13 @@
// Help Overlay // Help Overlay
// Purpose: Defines the Help Overlay module and the local helpers/components used in this file. // Purpose: Defines the Help Overlay module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import HelpContentView from '../HelpContentView/index.jsx'; import HelpContentView from '../HelpContentView/index.jsx';
export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onToggleShowOnLoad }) { export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onToggleShowOnLoad }) {
if (!visible) return null; const keymap = useControlSelector((control) => control.state.keymap);
const { state } = useControlSystem(); if (!visible) return null;
const handleCheckbox = (event) => { const handleCheckbox = (event) => {
const keepShowing = !event.target.checked; const keepShowing = !event.target.checked;
@@ -39,7 +39,7 @@ export default function HelpOverlay({ visible, layout, onClose, showOnLoad, onTo
</div> </div>
</div> </div>
<div className="max-h-[85vh] overflow-y-auto p-0.5"> <div className="max-h-[85vh] overflow-y-auto p-0.5">
<HelpContentView layout={layout} keymap={state?.keymap || {}} /> <HelpContentView layout={layout} keymap={keymap || {}} />
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,12 +1,12 @@
// Help Panel // Help Panel
// Purpose: Defines the Help Panel module and the local helpers/components used in this file. // Purpose: Defines the Help Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import CardFrame from '../CardFrame/index.jsx'; import CardFrame from '../CardFrame/index.jsx';
import HelpContentView from '../HelpContentView/index.jsx'; import HelpContentView from '../HelpContentView/index.jsx';
export default function HelpPanel({ layout, onOpenOverlay }) { export default function HelpPanel({ layout, onOpenOverlay }) {
const { state } = useControlSystem(); const keymap = useControlSelector((control) => control.state.keymap);
const actions = ( const actions = (
<button <button
@@ -20,7 +20,7 @@ export default function HelpPanel({ layout, onOpenOverlay }) {
return ( return (
<CardFrame title="Help" actions={actions} bodyClassName="space-y-0.5 text-sm"> <CardFrame title="Help" actions={actions} bodyClassName="space-y-0.5 text-sm">
<HelpContentView layout={layout} keymap={state?.keymap || {}} /> <HelpContentView layout={layout} keymap={keymap || {}} />
</CardFrame> </CardFrame>
); );
} }
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js';
import CardFrame from '../CardFrame/index.jsx'; import CardFrame from '../CardFrame/index.jsx';
@@ -169,9 +169,7 @@ function LampTile({ entity, connected, controlsLocked, onToggle, onSetColor, onS
} }
export default function HomeAssistantControls() { export default function HomeAssistantControls() {
const { const keymap = useControlSelector((control) => control.state.keymap);
state: { keymap },
} = useControlSystem();
const ha = useSessionSelector((state) => state.session?.homeAssistant || null); const ha = useSessionSelector((state) => state.session?.homeAssistant || null);
const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } = const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } =
useSessionActions(); useSessionActions();
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSessionSelector } from '../../../context/SessionContext.jsx'; import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useControlSystem } from '../../../controls/index.js'; import { useControlSelector } from '../../../controls/index.js';
import SocialButton from '../../SocialButton/index.jsx'; import SocialButton from '../../SocialButton/index.jsx';
function TurnsOverlay({ function TurnsOverlay({
@@ -15,9 +15,7 @@ function TurnsOverlay({
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {}); const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null); const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {}); const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
const { const lastControlIntentAt = useControlSelector((control) => control.state.lastControlIntentAt);
state: { lastControlIntentAt },
} = useControlSystem();
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
const [showTurnCue, setShowTurnCue] = useState(false); const [showTurnCue, setShowTurnCue] = useState(false);
const [turnCueStartAt, setTurnCueStartAt] = useState(null); const [turnCueStartAt, setTurnCueStartAt] = useState(null);
@@ -2,7 +2,7 @@
// Purpose: Defines the Keymap Settings module and the local helpers/components used in this file. // Purpose: Defines the Keymap Settings module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState, useEffect, useCallback } from 'react'; import { useMemo, useState, useEffect, useCallback } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import { DEFAULT_KEYMAP } from '../../controls/constants.js'; import { DEFAULT_KEYMAP } from '../../controls/constants.js';
import { canonicalizeKeyInput, formatKeyLabel } from '../../controls/keymapUtils.js'; import { canonicalizeKeyInput, formatKeyLabel } from '../../controls/keymapUtils.js';
import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js'; import { setKeyboardCaptureLocked } from '../../controls/inputs/keyboardCaptureLock.js';
@@ -191,10 +191,8 @@ function BindingRow({ action, value, isActive, onCaptureStart, onCancel, onCaptu
} }
export default function KeymapSettings() { export default function KeymapSettings() {
const { const keymap = useControlSelector((control) => control.state.keymap);
state: { keymap }, const { updateKeyBinding, resetKeyBindings } = useControlActions();
actions: { updateKeyBinding, resetKeyBindings },
} = useControlSystem();
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const keyboardSettings = { const keyboardSettings = {
...INPUT_SETTINGS_DEFAULTS.keyboard, ...INPUT_SETTINGS_DEFAULTS.keyboard,
@@ -2,7 +2,7 @@
// Purpose: Assembles the mobile auxiliary controls column, which is the left column by default. // Purpose: Assembles the mobile auxiliary controls column, which is the left column by default.
// Scope: Owns mobile aux/camera/night vision/horn wiring while reusing desktop variation components where intended. // Scope: Owns mobile aux/camera/night vision/horn wiring while reusing desktop variation components where intended.
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js'; import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import HornControl from '../HornControl/index.jsx'; import HornControl from '../HornControl/index.jsx';
import NightVisionControl from '../NightVisionControl/index.jsx'; import NightVisionControl from '../NightVisionControl/index.jsx';
@@ -11,19 +11,20 @@ import VacuumControls from './VacuumControls.jsx';
import VerticalCameraTilt from './VerticalCameraTilt.jsx'; import VerticalCameraTilt from './VerticalCameraTilt.jsx';
function AuxColumnContent() { function AuxColumnContent() {
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId, camera, horn }, const camera = useControlSelector((control) => control.state.camera);
pipeline, const horn = useControlSelector((control) => control.state.horn);
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn }, const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
} = useControlSystem(); const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
const pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
const { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn } = useControlActions();
const dockAssist = useManualDockAssist(); const dockAssist = useManualDockAssist();
const disabled = !roverId; const disabled = !roverId;
const activeAuxButtonRef = useRef(null); const activeAuxButtonRef = useRef(null);
const cameraConfig = camera?.config; const cameraConfig = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig); const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionAvailable = Boolean(roverId && nightVision);
const nightVisionState = pipeline?.nightVisionState; const hornAvailable = Boolean(roverId && pipelineHorn);
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornBlocked = horn?.overheated; const hornBlocked = horn?.overheated;
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45; const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45; const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
@@ -2,7 +2,7 @@
// Purpose: Provides the mobile movement control pad and speed mode selector. // Purpose: Provides the mobile movement control pad and speed mode selector.
// Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement. // Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import { normalizeKeymapEntries } from '../../controls/keymapUtils.js'; import { normalizeKeymapEntries } from '../../controls/keymapUtils.js';
import { import {
computeKeyboardDriveVector, computeKeyboardDriveVector,
@@ -29,10 +29,8 @@ function getSpeedModeConfig(speedMode) {
} }
export default function ControlPadPanel({ disabled = false }) { export default function ControlPadPanel({ disabled = false }) {
const { const rawKeymap = useControlSelector((control) => control.state.keymap);
state: { keymap: rawKeymap }, const { setDriveVector, registerInputState } = useControlActions();
actions: { setDriveVector, registerInputState },
} = useControlSystem();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const [speedMode, setSpeedMode] = useState('normal'); const [speedMode, setSpeedMode] = useState('normal');
const [activeInputLabel, setActiveInputLabel] = useState('stop'); const [activeInputLabel, setActiveInputLabel] = useState('stop');
@@ -1,14 +1,12 @@
// Movement Column // Movement Column
// Purpose: Assembles the mobile movement column, which is the right column by default. // Purpose: Assembles the mobile movement column, which is the right column by default.
// Scope: Integrates drive/dock actions with the mobile control pad without hiding that dependency inside the pad. // Scope: Integrates drive/dock actions with the mobile control pad without hiding that dependency inside the pad.
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx'; import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
import ControlPadPanel from './ControlPadPanel.jsx'; import ControlPadPanel from './ControlPadPanel.jsx';
function MovementColumnContent({ layout }) { function MovementColumnContent({ layout }) {
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId },
} = useControlSystem();
const driveDockState = useDriveDockState(roverId); const driveDockState = useDriveDockState(roverId);
const dockedNotDriving = driveDockState.docked && !driveDockState.driving; const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
const expandAction = dockedNotDriving || driveDockState.dockingInProgress; const expandAction = dockedNotDriving || driveDockState.dockingInProgress;
@@ -2,7 +2,7 @@
// Purpose: Defines the Overcurrent Limiter Panel module and the local helpers/components used in this file. // Purpose: Defines the Overcurrent Limiter Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js'; import { OVERCURRENT_GROUPS } from '../../controls/overcurrentLimiter.js';
import CardFrame from '../CardFrame/index.jsx'; import CardFrame from '../CardFrame/index.jsx';
@@ -26,10 +26,8 @@ function ProgressBar({ value, color = 'bg-emerald-500' }) {
} }
export default function OvercurrentLimiterPanel() { export default function OvercurrentLimiterPanel() {
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId }, const overcurrentLimiter = useControlSelector((control) => control.overcurrentLimiter);
overcurrentLimiter,
} = useControlSystem();
const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []); const groups = useMemo(() => OVERCURRENT_GROUPS.map((group) => group.key), []);
return ( return (
@@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlSelector } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js';
import NicknameForm from '../NicknameForm/index.jsx'; import NicknameForm from '../NicknameForm/index.jsx';
import SocialButton from '../SocialButton/index.jsx'; import SocialButton from '../SocialButton/index.jsx';
@@ -53,9 +53,9 @@ export default function QuickstartOverlay({
onOpenHelp, onOpenHelp,
onClose, onClose,
}) { }) {
const { state } = useControlSystem(); const rawKeymap = useControlSelector((control) => control.state.keymap);
const isDesktop = layout === 'desktop'; const isDesktop = layout === 'desktop';
const keymap = useMemo(() => state?.keymap || {}, [state?.keymap]); const keymap = useMemo(() => rawKeymap || {}, [rawKeymap]);
if (!visible) return null; if (!visible) return null;
+13 -14
View File
@@ -13,7 +13,7 @@ import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx';
import TopDownMap from '../TopDownMap/index.jsx'; import TopDownMap from '../TopDownMap/index.jsx';
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx'; import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx'; import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
import RawUserPilePanel from '../RawUserPilePanel/index.jsx'; import RawUserPilePanel from '../RawUserPilePanel/index.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js';
@@ -36,9 +36,7 @@ const CHAT_DOCK_MAX_HEIGHT = 300;
const CHAT_DOCK_BOTTOM_INSET = 8; const CHAT_DOCK_BOTTOM_INSET = 8;
function TopDownMapPanel() { function TopDownMapPanel() {
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId },
} = useControlSystem();
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
@@ -54,20 +52,22 @@ function TopDownMapPanel() {
} }
function DriveDockPanel() { function DriveDockPanel() {
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId, keymap, camera, horn }, const keymap = useControlSelector((control) => control.state.keymap);
pipeline, const camera = useControlSelector((control) => control.state.camera);
actions: { setServoAngle, setNightVision, startHorn, stopHorn }, const horn = useControlSelector((control) => control.state.horn);
} = useControlSystem(); const nightVision = useControlSelector((control) => control.pipeline?.nightVision);
const nightVisionState = useControlSelector((control) => control.pipeline?.nightVisionState);
const pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
const { setServoAngle, setNightVision, startHorn, stopHorn } = useControlActions();
const dockAssist = useManualDockAssist(); const dockAssist = useManualDockAssist();
const driveDockState = useDriveDockState(roverId); const driveDockState = useDriveDockState(roverId);
const hideInlineControls = driveDockState.docked && !driveDockState.driving; const hideInlineControls = driveDockState.docked && !driveDockState.driving;
const config = camera?.config; const config = camera?.config;
const cameraEnabled = Boolean(roverId && camera?.enabled && config); const cameraEnabled = Boolean(roverId && camera?.enabled && config);
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionAvailable = Boolean(roverId && nightVision);
const nightVisionState = pipeline?.nightVisionState; const hornAvailable = Boolean(roverId && pipelineHorn);
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornBlocked = horn?.overheated; const hornBlocked = horn?.overheated;
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30; const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30; const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
@@ -146,10 +146,9 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const roverId = String(state.session?.assignment?.roverId || '').trim(); const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null; return roverId ? state.session?.audioForward?.[roverId] || null : null;
}); });
const { state: controlState } = useControlSystem(); const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600'; const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600';
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled); const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live'; const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean( const vipMicActive = Boolean(
+4 -5
View File
@@ -2,7 +2,7 @@
// Purpose: Defines the Settings Panel module and the local helpers/components used in this file. // Purpose: Defines the Settings Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import AuthPanel from '../AuthPanel/index.jsx'; import AuthPanel from '../AuthPanel/index.jsx';
import AdminPanel from '../AdminPanel/index.jsx'; import AdminPanel from '../AdminPanel/index.jsx';
import KeymapSettings from '../KeymapSettings/index.jsx'; import KeymapSettings from '../KeymapSettings/index.jsx';
@@ -91,10 +91,9 @@ function RangeSetting({ label, value, disabled = false, onChange }) {
} }
export default function SettingsPanel() { export default function SettingsPanel() {
const { const keymap = useControlSelector((control) => control.state.keymap);
state: { keymap, roverId }, const roverId = useControlSelector((control) => control.state.roverId);
actions: { sendOiCommand, setSensorStream }, const { sendOiCommand, setSensorStream } = useControlActions();
} = useControlSystem();
const canControl = Boolean(roverId); const canControl = Boolean(roverId);
const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting(); const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting();
const socket = useSocket(); const socket = useSocket();
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { fieldClass } from '../constants.js'; import { fieldClass } from '../constants.js';
import { useControlSystem } from '../../../controls/index.js'; import { useControlSelector } from '../../../controls/index.js';
import { formatKeyLabel } from '../../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../../controls/keymapUtils.js';
import { useSettingsNamespace } from '../../../settings/index.js'; import { useSettingsNamespace } from '../../../settings/index.js';
import { MAX_UPLOAD_BYTES, TARGET_SAMPLE_RATE, RTC_CONFIG } from './constants.js'; import { MAX_UPLOAD_BYTES, TARGET_SAMPLE_RATE, RTC_CONFIG } from './constants.js';
@@ -28,7 +28,8 @@ export default function VipAudioUploadCard({
readyMicWhip, readyMicWhip,
stopMicWhip, stopMicWhip,
}) { }) {
const { state: controlState } = useControlSystem(); const keymap = useControlSelector((control) => control.state.keymap);
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', { const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', {
openMicEnabled: false, openMicEnabled: false,
pttMode: 'live', pttMode: 'live',
@@ -60,7 +61,6 @@ export default function VipAudioUploadCard({
const clipRecordingRef = useRef(false); const clipRecordingRef = useRef(false);
const clipSampleRateRef = useRef(TARGET_SAMPLE_RATE); const clipSampleRateRef = useRef(TARGET_SAMPLE_RATE);
const pttActive = Boolean(controlState?.mic?.pttActive);
const selectedForwardState = useMemo( const selectedForwardState = useMemo(
() => (roverId ? audioForwardByRover?.[roverId] || null : null), () => (roverId ? audioForwardByRover?.[roverId] || null : null),
@@ -81,7 +81,7 @@ export default function VipAudioUploadCard({
const whipLinkActive = !clipMode && (micState === 'live' || micState === 'starting'); const whipLinkActive = !clipMode && (micState === 'live' || micState === 'starting');
const clipRecording = clipMode && clipState === 'recording'; const clipRecording = clipMode && clipState === 'recording';
const clipSending = clipMode && clipState === 'sending'; const clipSending = clipMode && clipState === 'sending';
const pttKeyLabel = formatKeyLabel(controlState?.keymap?.micPtt?.[0]) || 'M'; const pttKeyLabel = formatKeyLabel(keymap?.micPtt?.[0]) || 'M';
const setPttMode = useCallback( const setPttMode = useCallback(
(nextMode) => { (nextMode) => {
+194 -23
View File
@@ -1,6 +1,17 @@
// Control Context Provider // Control Context Provider
// Purpose: Exposes control-system state/actions to control-capable components. Scope: Owns reducer wiring, pipeline integration, and top-level provider hooks. // Purpose: Exposes control-system state/actions to control-capable components. Scope: Owns reducer wiring, pipeline integration, and top-level provider hooks.
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react'; /* eslint-disable react-refresh/only-export-components */
import {
createContext,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
import { controlReducer, initialControlState } from './controlReducer.js'; import { controlReducer, initialControlState } from './controlReducer.js';
import { computeDifferentialSpeeds, clamp } from './controlMath.js'; import { computeDifferentialSpeeds, clamp } from './controlMath.js';
import { useCommandPipeline } from './commandPipeline.js'; import { useCommandPipeline } from './commandPipeline.js';
@@ -27,6 +38,35 @@ import {
const ControlSystemContext = createContext(null); const ControlSystemContext = createContext(null);
function normalizeSelector(selector) {
return typeof selector === 'function' ? selector : (snapshot) => snapshot;
}
const CONTROL_ACTION_NAMES = [
'setMode',
'setDriveVector',
'setAuxMotors',
'setServoAngle',
'nudgeServo',
'goServoHome',
'runMacro',
'stopAllMotion',
'sendOiCommand',
'setSensorStream',
'setNightVision',
'toggleNightVision',
'updateKeyBinding',
'resetKeyBindings',
'registerInputState',
'setManualDockAssistActive',
'toggleManualDockAssist',
'setSongNote',
'sendSong',
'startHorn',
'stopHorn',
'setMicPttActive',
];
function cloneKeymap(map) { function cloneKeymap(map) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(map || {}).map(([key, values]) => [key, Array.isArray(values) ? [...values] : []]), Object.entries(map || {}).map(([key, values]) => [key, Array.isArray(values) ? [...values] : []]),
@@ -59,6 +99,53 @@ function removeDriveSequenceBackoff(steps = []) {
export function ControlSystemProvider({ children }) { export function ControlSystemProvider({ children }) {
const [state, dispatch] = useReducer(controlReducer, initialControlState); const [state, dispatch] = useReducer(controlReducer, initialControlState);
const snapshotRef = useRef(null);
const subscribersRef = useRef(new Set());
const actionImplementationsRef = useRef({});
const [stableActions] = useState(() => {
/*
Public action functions intentionally keep stable identities. Each wrapper
reads the current implementation at call time, so consumers can depend on
actions without re-rendering just because the real implementation now
closes over a new state slice, pipeline object, or settings value.
*/
return Object.fromEntries(
CONTROL_ACTION_NAMES.map((name) => [
name,
(...args) => actionImplementationsRef.current[name]?.(...args),
]),
);
}, []);
const getControlSnapshot = useCallback(() => snapshotRef.current, []);
const subscribeControlSnapshot = useCallback((selector, listener, equalityFn = Object.is) => {
const normalizedSelector = normalizeSelector(selector);
const currentSnapshot = snapshotRef.current;
const subscriber = {
selector: normalizedSelector,
equalityFn,
listener,
current: normalizedSelector(currentSnapshot),
};
subscribersRef.current.add(subscriber);
return () => {
subscribersRef.current.delete(subscriber);
};
}, []);
const notifyControlSubscribers = useCallback((nextSnapshot) => {
/*
The provider context value below is stable, so React will not fan out an
update to every consumer automatically. This loop is the replacement: it
asks each subscribed component whether the exact value it selected changed
and only wakes that component when its selected value is different.
*/
subscribersRef.current.forEach((subscriber) => {
const nextSelected = subscriber.selector(nextSnapshot);
if (!subscriber.equalityFn(subscriber.current, nextSelected)) {
subscriber.current = nextSelected;
subscriber.listener(nextSelected);
}
});
}, []);
const prevModeRef = useRef(null); const prevModeRef = useRef(null);
const pendingLightsRef = useRef(false); const pendingLightsRef = useRef(false);
const servoAngleRef = useRef(initialControlState.camera.angle); const servoAngleRef = useRef(initialControlState.camera.angle);
@@ -166,7 +253,7 @@ export function ControlSystemProvider({ children }) {
if (pipeline.roverId) { if (pipeline.roverId) {
pipeline.enableSensorStream(); pipeline.enableSensorStream();
} }
}, [pipeline.roverId, pipeline.enableSensorStream]); }, [pipeline]);
const setMode = useCallback( const setMode = useCallback(
(mode) => { (mode) => {
@@ -430,6 +517,8 @@ export function ControlSystemProvider({ children }) {
return { waveform, freqs: normalized }; return { waveform, freqs: normalized };
}, [hornSettings]); }, [hornSettings]);
const stopHornRef = useRef(null);
const startHorn = useCallback(() => { const startHorn = useCallback(() => {
if (!pipeline.horn) return; if (!pipeline.horn) return;
if (state.horn?.overheated) return false; if (state.horn?.overheated) return false;
@@ -445,7 +534,7 @@ export function ControlSystemProvider({ children }) {
hornAutoStopRef.current = null; hornAutoStopRef.current = null;
} }
hornAutoStopRef.current = setTimeout(() => { hornAutoStopRef.current = setTimeout(() => {
stopHorn(); stopHornRef.current?.();
}, HORN_MAX_MS); }, HORN_MAX_MS);
recordControlIntent(); recordControlIntent();
return true; return true;
@@ -474,15 +563,13 @@ export function ControlSystemProvider({ children }) {
} }
dispatch({ type: 'control/set-horn-heat', payload: { heat: nextHeat, overheated: nextOverheated } }); dispatch({ type: 'control/set-horn-heat', payload: { heat: nextHeat, overheated: nextOverheated } });
} }
}, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated, HORN_HEAT_UP_PER_SEC]); }, [dispatch, pipeline, state.horn?.heat, state.horn?.overheated]);
const hornStateRef = useRef({ const hornStateRef = useRef({
active: Boolean(state.horn?.active), active: Boolean(state.horn?.active),
heat: state.horn?.heat ?? 0, heat: state.horn?.heat ?? 0,
overheated: Boolean(state.horn?.overheated), overheated: Boolean(state.horn?.overheated),
}); });
const stopHornRef = useRef(stopHorn);
useEffect(() => { useEffect(() => {
hornStateRef.current = { hornStateRef.current = {
active: Boolean(state.horn?.active), active: Boolean(state.horn?.active),
@@ -525,7 +612,7 @@ export function ControlSystemProvider({ children }) {
} }
}, tickMs); }, tickMs);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [dispatch, hornNeedsTick, HORN_HEAT_COOL_PER_SEC, HORN_HEAT_RESUME_THRESHOLD, HORN_HEAT_UP_PER_SEC]); }, [dispatch, hornNeedsTick]);
const setManualDockAssistActive = useCallback( const setManualDockAssistActive = useCallback(
(active) => { (active) => {
@@ -561,13 +648,8 @@ export function ControlSystemProvider({ children }) {
dispatch({ type: 'control/set-mic-ptt', payload: Boolean(active) }); dispatch({ type: 'control/set-mic-ptt', payload: Boolean(active) });
}, []); }, []);
const contextValue = useMemo( const actionImplementations = useMemo(
() => ({ () => ({
state,
dispatch,
pipeline,
overcurrentLimiter,
actions: {
setMode, setMode,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
@@ -590,12 +672,8 @@ export function ControlSystemProvider({ children }) {
startHorn, startHorn,
stopHorn, stopHorn,
setMicPttActive, setMicPttActive,
},
}), }),
[ [
state,
pipeline,
overcurrentLimiter,
setMode, setMode,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
@@ -621,13 +699,106 @@ export function ControlSystemProvider({ children }) {
], ],
); );
return <ControlSystemContext.Provider value={contextValue}>{children}</ControlSystemContext.Provider>; const snapshot = useMemo(
() => ({
state,
dispatch,
pipeline,
overcurrentLimiter,
actions: stableActions,
}),
[state, pipeline, overcurrentLimiter, stableActions],
);
if (snapshotRef.current == null) {
/*
The first render has to make a snapshot available synchronously because
descendants can call selector hooks during that same render pass. Later
renders publish updates from the layout effect below, after React has
committed the provider's new state.
*/
snapshotRef.current = snapshot;
}
useLayoutEffect(() => {
actionImplementationsRef.current = actionImplementations;
}, [actionImplementations]);
useLayoutEffect(() => {
if (snapshotRef.current === snapshot) return;
snapshotRef.current = snapshot;
notifyControlSubscribers(snapshot);
}, [notifyControlSubscribers, snapshot]);
const store = useMemo(
() => ({
getSnapshot: getControlSnapshot,
subscribe: subscribeControlSnapshot,
actions: stableActions,
}),
[getControlSnapshot, stableActions, subscribeControlSnapshot],
);
return <ControlSystemContext.Provider value={store}>{children}</ControlSystemContext.Provider>;
}
export function useControlSelector(selector, equalityFn = Object.is) {
const store = useContext(ControlSystemContext);
if (!store) {
throw new Error('useControlSelector must be used within ControlSystemProvider');
}
const selectorRef = useRef(selector);
const equalityRef = useRef(equalityFn);
const [selected, setSelected] = useState(() => selector(store.getSnapshot()));
useEffect(() => {
/*
Selector functions are often declared inline at the call site. Refreshing
these refs from an effect keeps the subscription callback pointed at the
newest selector/equality pair without making render mutate refs.
*/
selectorRef.current = selector;
equalityRef.current = equalityFn;
}, [equalityFn, selector]);
useEffect(() => {
/*
Recheck once after subscribing because the provider may have published a
newer snapshot between this component's render and its effect. The equality
guard keeps that synchronization from causing a redundant render.
*/
setSelected((prev) => {
const next = selectorRef.current(store.getSnapshot());
return equalityRef.current(prev, next) ? prev : next;
});
return store.subscribe(
(snapshot) => selectorRef.current(snapshot),
(nextSelected) => {
setSelected((prev) => (equalityRef.current(prev, nextSelected) ? prev : nextSelected));
},
(a, b) => equalityRef.current(a, b),
);
}, [store]);
return selected;
}
export function useControlActions() {
const store = useContext(ControlSystemContext);
if (!store) {
throw new Error('useControlActions must be used within ControlSystemProvider');
}
return store.actions;
} }
export function useControlSystem() { export function useControlSystem() {
const context = useContext(ControlSystemContext); /*
if (!context) { This compatibility hook intentionally selects the whole snapshot, so it has
throw new Error('useControlSystem must be used within ControlSystemProvider'); the same broad update behavior as the old context API. New and migrated
} consumers should prefer useControlSelector/useControlActions so they only
return context; re-render for data they actually read.
*/
return useControlSelector((snapshot) => snapshot);
} }
+6 -1
View File
@@ -1,6 +1,11 @@
// Control Module Exports // Control Module Exports
// Purpose: Re-exports control context/providers and input managers from one entrypoint. Scope: Keeps control imports stable and concise for app/module consumers. // Purpose: Re-exports control context/providers and input managers from one entrypoint. Scope: Keeps control imports stable and concise for app/module consumers.
export { ControlSystemProvider, useControlSystem } from './ControlContext.jsx'; export {
ControlSystemProvider,
useControlActions,
useControlSelector,
useControlSystem,
} from './ControlContext.jsx';
export { default as KeyboardInputManager } from './inputs/KeyboardInputManager.jsx'; export { default as KeyboardInputManager } from './inputs/KeyboardInputManager.jsx';
export { default as GamepadInputManager } from './inputs/GamepadInputManager.jsx'; export { default as GamepadInputManager } from './inputs/GamepadInputManager.jsx';
export { useOvercurrentLimiter } from './overcurrentLimiter.js'; export { useOvercurrentLimiter } from './overcurrentLimiter.js';
@@ -1,7 +1,7 @@
// Gamepad Input Manager // Gamepad Input Manager
// Purpose: Converts polled gamepad state into normalized control actions/commands. Scope: Integrates bindings, deadzone math, and dispatch callbacks for driving. // Purpose: Converts polled gamepad state into normalized control actions/commands. Scope: Integrates bindings, deadzone math, and dispatch callbacks for driving.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../ControlContext.jsx'; import { useControlActions, useControlSelector } from '../ControlContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { GAMEPAD_SETTINGS_DEFAULTS, GAMEPAD_PROFILE_DEFAULT } from '../../settings/namespaces.js'; import { GAMEPAD_SETTINGS_DEFAULTS, GAMEPAD_PROFILE_DEFAULT } from '../../settings/namespaces.js';
import { import {
@@ -48,8 +48,6 @@ function pickActivePad(pads, activeSignature) {
export default function GamepadInputManager() { export default function GamepadInputManager() {
const { const {
state,
actions: {
setMode, setMode,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
@@ -57,8 +55,9 @@ export default function GamepadInputManager() {
runMacro, runMacro,
toggleNightVision, toggleNightVision,
registerInputState, registerInputState,
}, } = useControlActions();
} = useControlSystem(); const cameraAngle = useControlSelector((control) => control.state.camera?.angle);
const cameraConfig = useControlSelector((control) => control.state.camera?.config);
const dockAssist = useManualDockAssist(); const dockAssist = useManualDockAssist();
const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace( const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace(
'gamepad', 'gamepad',
@@ -162,8 +161,8 @@ export default function GamepadInputManager() {
// with the newest settings and control actions without resubscribing to the hub. // with the newest settings and control actions without resubscribing to the hub.
latestRef.current = { latestRef.current = {
activeSignature, activeSignature,
cameraAngle: state.camera?.angle, cameraAngle,
cameraConfig: state.camera?.config, cameraConfig,
dockAssist, dockAssist,
gamepadSettings, gamepadSettings,
registerInputState, registerInputState,
@@ -1,7 +1,7 @@
// Keyboard Input Manager // Keyboard Input Manager
// Purpose: Captures and translates keyboard events into normalized control intents. Scope: Owns keydown/keyup listeners and dispatch coordination for drive controls. // Purpose: Captures and translates keyboard events into normalized control intents. Scope: Owns keydown/keyup listeners and dispatch coordination for drive controls.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../ControlContext.jsx'; import { useControlActions, useControlSelector } from '../ControlContext.jsx';
import { useChat } from '../../context/ChatContext.jsx'; import { useChat } from '../../context/ChatContext.jsx';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js'; import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
@@ -83,8 +83,6 @@ function shouldIgnoreEvent(event) {
export default function KeyboardInputManager() { export default function KeyboardInputManager() {
const { const {
state,
actions: {
setMode, setMode,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
@@ -98,15 +96,19 @@ export default function KeyboardInputManager() {
setMicPttActive, setMicPttActive,
setSongNote, setSongNote,
sendSong, sendSong,
}, } = useControlActions();
} = useControlSystem(); const rawKeymap = useControlSelector((control) => control.state.keymap);
const cameraNudgeDegrees = useControlSelector((control) => control.state.camera?.config?.nudgeDegrees);
const songNote = useControlSelector((control) => control.state.song?.note);
const roverId = useControlSelector((control) => control.state.roverId);
const hornActive = useControlSelector((control) => Boolean(control.state.horn?.active));
const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null); const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
const dockAssist = useManualDockAssist(); const dockAssist = useManualDockAssist();
const { homeAssistantSetState } = useSessionActions(); const { homeAssistantSetState } = useSessionActions();
const { focusChat, isChatFocused } = useChat(); const { focusChat, isChatFocused } = useChat();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
const { save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS); const { save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]); const keymap = useMemo(() => normalizeKeymapEntries(rawKeymap), [rawKeymap]);
const actionTokens = useMemo(() => { const actionTokens = useMemo(() => {
const tokens = new Set(); const tokens = new Set();
Object.values(keymap).forEach((bindingSet) => { Object.values(keymap).forEach((bindingSet) => {
@@ -132,8 +134,8 @@ export default function KeyboardInputManager() {
return mapTiltSpeedToInterval(tiltSpeed); return mapTiltSpeedToInterval(tiltSpeed);
}, [inputSettings?.keyboard]); }, [inputSettings?.keyboard]);
const servoStep = useMemo( const servoStep = useMemo(
() => Math.abs(state.camera?.config?.nudgeDegrees || 1), () => Math.abs(cameraNudgeDegrees || 1),
[state.camera?.config?.nudgeDegrees], [cameraNudgeDegrees],
); );
const activeTokensRef = useRef(new Set()); const activeTokensRef = useRef(new Set());
@@ -352,7 +354,7 @@ export default function KeyboardInputManager() {
setMicPttActive, setMicPttActive,
setMode, setMode,
setSongNote, setSongNote,
songNote: state.song?.note, songNote,
startHorn, startHorn,
stopAllMotion, stopAllMotion,
stopHorn, stopHorn,
@@ -455,11 +457,11 @@ export default function KeyboardInputManager() {
useEffect(() => { useEffect(() => {
latestResetAllRef.current(); latestResetAllRef.current();
}, [state.roverId]); }, [roverId]);
useEffect(() => { useEffect(() => {
hornActiveRef.current = Boolean(state.horn?.active); hornActiveRef.current = hornActive;
}, [state.horn?.active]); }, [hornActive]);
useEffect( useEffect(
() => () => { () => () => {
@@ -1,19 +1,17 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlActions, useControlSelector } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
export function useManualDockAssist(options = {}) { export function useManualDockAssist(options = {}) {
const { manageLifecycle = false } = options; const { manageLifecycle = false } = options;
const { const roverId = useControlSelector((control) => control.state.roverId);
state: { roverId, manualDockAssist }, const active = useControlSelector((control) => Boolean(control.state.manualDockAssist?.active));
actions, const actions = useControlActions();
} = useControlSystem();
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
const chargingLabel = sensors?.chargingState?.label || ''; const chargingLabel = sensors?.chargingState?.label || '';
const docked = Boolean(sensors?.chargingSources?.homeBase); const docked = Boolean(sensors?.chargingSources?.homeBase);
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== ''; const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
const active = Boolean(manualDockAssist?.active);
const wasDockedRef = useRef(false); const wasDockedRef = useRef(false);
const enterAssist = useCallback(() => { const enterAssist = useCallback(() => {