@@ -36,130 +36,151 @@ function broadcast(data) {
3636export function createPanelServer ( port ) {
3737 const html = getHTML ( ) ;
3838 const server = http . createServer ( async ( req , res ) => {
39- const url = new URL ( req . url || '/' , `http://localhost:${ port } ` ) ;
40- const p = url . pathname ;
41- // ─── HTML ──
42- if ( p === '/' ) {
43- res . writeHead ( 200 , {
44- 'Content-Type' : 'text/html; charset=utf-8' ,
45- 'Cache-Control' : 'no-store, no-cache, must-revalidate' ,
46- 'Pragma' : 'no-cache' ,
47- } ) ;
48- res . end ( html ) ;
49- return ;
50- }
51- // ─── Static assets ──
52- if ( p . startsWith ( '/assets/' ) && p . endsWith ( '.jpg' ) ) {
53- const filename = path . basename ( p ) ;
54- const assetsDir = path . join ( path . dirname ( path . dirname ( new URL ( import . meta. url ) . pathname ) ) , '..' , 'assets' ) ;
55- const imgPath = path . join ( assetsDir , filename ) ;
56- try {
57- const img = fs . readFileSync ( imgPath ) ;
58- res . writeHead ( 200 , {
59- 'Content-Type' : 'image/jpeg' ,
60- 'Cache-Control' : 'public, max-age=86400' ,
61- } ) ;
62- res . end ( img ) ;
63- }
64- catch {
65- res . writeHead ( 404 ) ;
66- res . end ( 'Not found' ) ;
67- }
68- return ;
69- }
70- // ─── SSE ──
71- if ( p === '/api/events' ) {
72- res . writeHead ( 200 , {
73- 'Content-Type' : 'text/event-stream' ,
74- 'Cache-Control' : 'no-cache' ,
75- 'Connection' : 'keep-alive' ,
76- 'Access-Control-Allow-Origin' : '*' ,
77- } ) ;
78- res . write ( 'data: {"type":"connected"}\n\n' ) ;
79- sseClients . add ( res ) ;
80- req . on ( 'close' , ( ) => sseClients . delete ( res ) ) ;
81- return ;
82- }
83- // ─── API ──
8439 try {
85- if ( p === '/api/stats' ) {
86- const summary = getStatsSummary ( ) ;
87- json ( res , {
88- totalRequests : summary . stats . totalRequests ,
89- totalCostUsd : summary . stats . totalCostUsd ,
90- opusCost : summary . opusCost ,
91- saved : summary . saved ,
92- savedPct : summary . savedPct ,
93- avgCostPerRequest : summary . avgCostPerRequest ,
94- period : summary . period ,
95- byModel : summary . stats . byModel ,
40+ const url = new URL ( req . url || '/' , `http://localhost:${ port } ` ) ;
41+ const p = url . pathname ;
42+ // ─── HTML ──
43+ if ( p === '/' ) {
44+ res . writeHead ( 200 , {
45+ 'Content-Type' : 'text/html; charset=utf-8' ,
46+ 'Cache-Control' : 'no-store, no-cache, must-revalidate' ,
47+ 'Pragma' : 'no-cache' ,
9648 } ) ;
49+ res . end ( html ) ;
9750 return ;
9851 }
99- if ( p === '/api/insights' ) {
100- const days = parseInt ( url . searchParams . get ( 'days' ) || '30' , 10 ) ;
101- const report = generateInsights ( days ) ;
102- json ( res , report ) ;
103- return ;
104- }
105- if ( p === '/api/sessions' ) {
106- const sessions = listSessions ( ) ;
107- json ( res , sessions ) ;
108- return ;
109- }
110- if ( p . startsWith ( '/api/sessions/search' ) ) {
111- const q = url . searchParams . get ( 'q' ) || '' ;
112- const limit = parseInt ( url . searchParams . get ( 'limit' ) || '20' , 10 ) ;
113- const results = searchSessions ( q , { limit } ) ;
114- json ( res , results ) ;
52+ // ─── Static assets ──
53+ if ( p . startsWith ( '/assets/' ) && p . endsWith ( '.jpg' ) ) {
54+ const filename = path . basename ( p ) ;
55+ const assetsDir = path . join ( path . dirname ( path . dirname ( new URL ( import . meta. url ) . pathname ) ) , '..' , 'assets' ) ;
56+ const imgPath = path . join ( assetsDir , filename ) ;
57+ try {
58+ const img = fs . readFileSync ( imgPath ) ;
59+ res . writeHead ( 200 , {
60+ 'Content-Type' : 'image/jpeg' ,
61+ 'Cache-Control' : 'public, max-age=86400' ,
62+ } ) ;
63+ res . end ( img ) ;
64+ }
65+ catch {
66+ res . writeHead ( 404 ) ;
67+ res . end ( 'Not found' ) ;
68+ }
11569 return ;
11670 }
117- if ( p . startsWith ( '/api/sessions/' ) ) {
118- const id = decodeURIComponent ( p . slice ( '/api/sessions/' . length ) ) ;
119- const history = loadSessionHistory ( id ) ;
120- json ( res , history ) ;
71+ // ─── SSE ──
72+ if ( p === '/api/events' ) {
73+ res . writeHead ( 200 , {
74+ 'Content-Type' : 'text/event-stream' ,
75+ 'Cache-Control' : 'no-cache' ,
76+ 'Connection' : 'keep-alive' ,
77+ 'Access-Control-Allow-Origin' : '*' ,
78+ } ) ;
79+ res . write ( 'data: {"type":"connected"}\n\n' ) ;
80+ sseClients . add ( res ) ;
81+ req . on ( 'close' , ( ) => sseClients . delete ( res ) ) ;
12182 return ;
12283 }
123- if ( p === '/api/wallet' ) {
124- try {
125- const chain = loadChain ( ) ;
126- let address = '' , balance = 0 ;
127- if ( chain === 'solana' ) {
128- const { setupAgentSolanaWallet } = await import ( '@blockrun/llm' ) ;
129- const client = await setupAgentSolanaWallet ( { silent : true } ) ;
130- address = await client . getWalletAddress ( ) ;
131- balance = await client . getBalance ( ) ;
84+ // ─── API ──
85+ try {
86+ if ( p === '/api/stats' ) {
87+ const summary = getStatsSummary ( ) ;
88+ json ( res , {
89+ totalRequests : summary . stats . totalRequests ,
90+ totalCostUsd : summary . stats . totalCostUsd ,
91+ opusCost : summary . opusCost ,
92+ saved : summary . saved ,
93+ savedPct : summary . savedPct ,
94+ avgCostPerRequest : summary . avgCostPerRequest ,
95+ period : summary . period ,
96+ byModel : summary . stats . byModel ,
97+ } ) ;
98+ return ;
99+ }
100+ if ( p === '/api/insights' ) {
101+ const days = parseInt ( url . searchParams . get ( 'days' ) || '30' , 10 ) ;
102+ const report = generateInsights ( days ) ;
103+ json ( res , report ) ;
104+ return ;
105+ }
106+ if ( p === '/api/sessions' ) {
107+ const sessions = listSessions ( ) ;
108+ json ( res , sessions ) ;
109+ return ;
110+ }
111+ if ( p . startsWith ( '/api/sessions/search' ) ) {
112+ const q = url . searchParams . get ( 'q' ) || '' ;
113+ const limit = parseInt ( url . searchParams . get ( 'limit' ) || '20' , 10 ) ;
114+ const results = searchSessions ( q , { limit } ) ;
115+ json ( res , results ) ;
116+ return ;
117+ }
118+ if ( p . startsWith ( '/api/sessions/' ) ) {
119+ const id = decodeURIComponent ( p . slice ( '/api/sessions/' . length ) ) ;
120+ const history = loadSessionHistory ( id ) ;
121+ json ( res , history ) ;
122+ return ;
123+ }
124+ if ( p === '/api/wallet' ) {
125+ try {
126+ const chain = loadChain ( ) ;
127+ let address = '' , balance = 0 ;
128+ if ( chain === 'solana' ) {
129+ const { setupAgentSolanaWallet } = await import ( '@blockrun/llm' ) ;
130+ const client = await setupAgentSolanaWallet ( { silent : true } ) ;
131+ address = await client . getWalletAddress ( ) ;
132+ balance = await client . getBalance ( ) ;
133+ }
134+ else {
135+ const { setupAgentWallet } = await import ( '@blockrun/llm' ) ;
136+ const client = setupAgentWallet ( { silent : true } ) ;
137+ address = client . getWalletAddress ( ) ;
138+ balance = await client . getBalance ( ) ;
139+ }
140+ json ( res , { address, balance, chain } ) ;
132141 }
133- else {
134- const { setupAgentWallet } = await import ( '@blockrun/llm' ) ;
135- const client = setupAgentWallet ( { silent : true } ) ;
136- address = client . getWalletAddress ( ) ;
137- balance = await client . getBalance ( ) ;
142+ catch {
143+ json ( res , { address : 'not set' , balance : 0 , chain : loadChain ( ) } ) ;
138144 }
139- json ( res , { address , balance , chain } ) ;
145+ return ;
140146 }
141- catch {
142- json ( res , { address : 'not set' , balance : 0 , chain : loadChain ( ) } ) ;
147+ if ( p === '/api/social' ) {
148+ const stats = getSocialStats ( ) ;
149+ json ( res , stats ) ;
150+ return ;
143151 }
144- return ;
145- }
146- if ( p === '/api/social' ) {
147- const stats = getSocialStats ( ) ;
148- json ( res , stats ) ;
149- return ;
152+ if ( p === '/api/learnings' ) {
153+ const learnings = loadLearnings ( ) ;
154+ json ( res , learnings ) ;
155+ return ;
156+ }
157+ // 404
158+ res . writeHead ( 404 ) ;
159+ res . end ( 'Not found' ) ;
150160 }
151- if ( p === '/api/learnings' ) {
152- const learnings = loadLearnings ( ) ;
153- json ( res , learnings ) ;
154- return ;
161+ catch ( err ) {
162+ json ( res , { error : err . message } , 500 ) ;
155163 }
156- // 404
157- res . writeHead ( 404 ) ;
158- res . end ( 'Not found' ) ;
159164 }
160165 catch ( err ) {
161- json ( res , { error : err . message } , 500 ) ;
166+ // Outer safety net — logs but never crashes the server
167+ try {
168+ if ( ! res . headersSent )
169+ json ( res , { error : err . message } , 500 ) ;
170+ else
171+ res . end ( ) ;
172+ }
173+ catch { /* socket already gone */ }
174+ console . error ( '[panel] request error:' , err . message ) ;
175+ }
176+ } ) ;
177+ // Swallow socket errors (client disconnects, etc.) so they don't crash the process
178+ server . on ( 'clientError' , ( err , socket ) => {
179+ try {
180+ socket . destroy ( ) ;
162181 }
182+ catch { /* already closed */ }
183+ console . error ( '[panel] client error:' , err . message ) ;
163184 } ) ;
164185 // Watch stats file for changes → push to SSE clients
165186 const statsFile = fs . existsSync ( path . join ( BLOCKRUN_DIR , 'franklin-stats.json' ) )
0 commit comments