@@ -38,6 +38,105 @@ function ImagePreloader() {
3838 )
3939}
4040
41+ // Capture console logs for debug reports
42+ const consoleLogs = [ ]
43+ const MAX_LOGS = 100
44+ const originalConsole = { log : console . log , warn : console . warn , error : console . error , info : console . info , debug : console . debug }
45+ ; [ 'log' , 'warn' , 'error' , 'info' , 'debug' ] . forEach ( level => {
46+ console [ level ] = ( ...args ) => {
47+ consoleLogs . push ( { level, time : new Date ( ) . toISOString ( ) , message : args . map ( a => typeof a === 'object' ? JSON . stringify ( a ) : String ( a ) ) . join ( ' ' ) } )
48+ if ( consoleLogs . length > MAX_LOGS ) consoleLogs . shift ( )
49+ originalConsole [ level ] ?. ( ...args )
50+ }
51+ } )
52+
53+ // Debug info component for error reporting
54+ function DebugInfo ( { error, step, selectedDevice, serial, message } ) {
55+ const [ copied , setCopied ] = useState ( false )
56+
57+ const getDebugReport = ( ) => {
58+ const deviceName = selectedDevice === DeviceType . COMMA_4 ? 'comma four' : selectedDevice === DeviceType . COMMA_3 ? 'comma 3/3X' : 'unknown'
59+ const errorName = Object . keys ( ErrorCode ) . find ( k => ErrorCode [ k ] === error ) || 'UNKNOWN'
60+ const stepName = Object . keys ( StepCode ) . find ( k => StepCode [ k ] === step ) || 'UNKNOWN'
61+
62+ // Get detailed OS info
63+ const ua = navigator . userAgent
64+ let os = 'Unknown'
65+ if ( ua . includes ( 'Windows NT 10.0' ) ) os = 'Windows 10/11'
66+ else if ( ua . includes ( 'Windows NT 6.3' ) ) os = 'Windows 8.1'
67+ else if ( ua . includes ( 'Windows NT 6.2' ) ) os = 'Windows 8'
68+ else if ( ua . includes ( 'Windows NT 6.1' ) ) os = 'Windows 7'
69+ else if ( ua . includes ( 'Mac OS X' ) ) {
70+ const match = ua . match ( / M a c O S X ( \d + [ . _ ] \d + [ . _ ] ? \d * ) / )
71+ os = match ? `macOS ${ match [ 1 ] . replace ( / _ / g, '.' ) } ` : 'macOS'
72+ } else if ( ua . includes ( 'Linux' ) ) {
73+ os = 'Linux'
74+ if ( ua . includes ( 'Ubuntu' ) ) os += ' (Ubuntu)'
75+ else if ( ua . includes ( 'Fedora' ) ) os += ' (Fedora)'
76+ else if ( ua . includes ( 'Debian' ) ) os += ' (Debian)'
77+ } else if ( ua . includes ( 'CrOS' ) ) os = 'ChromeOS'
78+
79+ // Detect sandboxed browsers
80+ const sandboxHints = [ ]
81+ if ( ua . includes ( 'snap' ) ) sandboxHints . push ( 'Snap' )
82+ if ( ua . includes ( 'Flatpak' ) ) sandboxHints . push ( 'Flatpak' )
83+ if ( navigator . userAgentData ?. brands ?. some ( b => b . brand . includes ( 'snap' ) ) ) sandboxHints . push ( 'Snap' )
84+ // Snap Chrome often has restricted /dev access which breaks WebUSB
85+ if ( isLinux && ! navigator . usb ) sandboxHints . push ( 'WebUSB unavailable - possibly sandboxed' )
86+ const sandbox = sandboxHints . length ? sandboxHints . join ( ', ' ) : 'None detected'
87+
88+ return `## Bug Report - flash.comma.ai
89+
90+ **Device:** ${ deviceName }
91+ **Serial:** ${ serial || 'N/A' }
92+ **Error:** ${ errorName }
93+ **Step:** ${ stepName }
94+ **Last Message:** ${ message || 'N/A' }
95+
96+ **OS:** ${ os }
97+ **Sandbox:** ${ sandbox }
98+ **Browser:** ${ navigator . userAgent }
99+ **URL:** ${ window . location . href }
100+ **Time:** ${ new Date ( ) . toISOString ( ) }
101+
102+ <details>
103+ <summary>Console Logs</summary>
104+
105+ \`\`\`
106+ ${ consoleLogs . slice ( - 30 ) . map ( l => `[${ l . time } ] [${ l . level } ] ${ l . message } ` ) . join ( '\n' ) }
107+ \`\`\`
108+
109+ </details>
110+ `
111+ }
112+
113+ const handleCopy = ( ) => {
114+ navigator . clipboard . writeText ( getDebugReport ( ) )
115+ setCopied ( true )
116+ setTimeout ( ( ) => setCopied ( false ) , 2000 )
117+ }
118+
119+ return (
120+ < div className = "mt-6 w-full max-w-xl p-4 bg-gray-100 rounded-lg text-left text-sm" >
121+ < p className = "text-gray-600 mb-3" >
122+ Copy this debug info and paste it in{ ' ' }
123+ < a href = "https://discord.comma.ai" target = "_blank" rel = "noopener noreferrer" className = "text-blue-500 hover:underline" > Discord</ a >
124+ { ' ' } or{ ' ' }
125+ < a href = "https://github.com/commaai/flash/issues/new" target = "_blank" rel = "noopener noreferrer" className = "text-blue-500 hover:underline" > GitHub Issues</ a > .
126+ </ p >
127+ < pre className = "bg-gray-900 text-gray-100 p-3 rounded text-xs overflow-auto max-h-48 font-mono debug-scrollbar" >
128+ { getDebugReport ( ) }
129+ </ pre >
130+ < button
131+ onClick = { handleCopy }
132+ className = "mt-3 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm rounded transition-colors"
133+ >
134+ { copied ? 'Copied!' : 'Copy Debug Info' }
135+ </ button >
136+ </ div >
137+ )
138+ }
139+
41140
42141const steps = {
43142 [ StepCode . INITIALIZING ] : {
@@ -740,6 +839,9 @@ export default function Flash() {
740839 </ button >
741840 ) }
742841 { connected && < DeviceState serial = { serial } /> }
842+ { error !== ErrorCode . NONE && (
843+ < DebugInfo error = { error } step = { step } selectedDevice = { selectedDevice } serial = { serial } message = { message } />
844+ ) }
743845 </ div >
744846 )
745847}
0 commit comments