reactoradar 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/app.js +83 -26
  2. package/main.js +111 -43
  3. package/package.json +1 -1
  4. package/styles.css +11 -6
package/app.js CHANGED
@@ -209,7 +209,7 @@ if (window.electronAPI) {
209
209
  const btn = $('btnCDP');
210
210
  if (btn) {
211
211
  const hasCDP = targets?.length > 0;
212
- const port = getStoredMetroPort();
212
+ const port = state.ports?.METRO || getStoredMetroPort();
213
213
  btn.textContent = hasCDP
214
214
  ? `JS Debugger (:${port}) [${targets.length}] ↗`
215
215
  : `JS Debugger (:${port}) ↗`;
@@ -268,21 +268,7 @@ if (window.electronAPI) {
268
268
  window.electronAPI.on('update-available', ({ current, latest }) => {
269
269
  // Show in settings only, not as a banner
270
270
  state._updateAvailable = { current, latest };
271
- const el = $('aboutVersion');
272
- if (el) el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
273
- // Add update button in settings if not already there
274
- if (!$('updateBtn')) {
275
- const aboutEl = document.querySelector('.settings-about');
276
- if (aboutEl) {
277
- const btn = document.createElement('div');
278
- btn.style.cssText = 'margin-top:10px';
279
- btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px">Download v' + latest + '</button>';
280
- aboutEl.appendChild(btn);
281
- $('updateBtn')?.addEventListener('click', () => {
282
- window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
283
- });
284
- }
285
- }
271
+ _applyUpdateBanner();
286
272
  });
287
273
 
288
274
  window.electronAPI.on('trigger-open-cdp', () => {
@@ -299,6 +285,31 @@ if (window.electronAPI) {
299
285
  }
300
286
 
301
287
  // ─── Device Connection Status (inline in titlebar) ───────────────────────────
288
+ // Reusable — called from IPC handler AND from initSettingsPanel
289
+ function _applyUpdateBanner() {
290
+ const info = state._updateAvailable;
291
+ if (!info) return;
292
+ const { current, latest } = info;
293
+ const el = $('aboutVersion');
294
+ if (el && !el.dataset.updateApplied) {
295
+ el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
296
+ el.dataset.updateApplied = '1';
297
+ }
298
+ // Add update button in settings if not already there
299
+ if (!$('updateBtn')) {
300
+ const aboutEl = document.querySelector('.settings-about');
301
+ if (aboutEl) {
302
+ const btn = document.createElement('div');
303
+ btn.style.cssText = 'margin-top:10px';
304
+ btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
305
+ aboutEl.appendChild(btn);
306
+ $('updateBtn')?.addEventListener('click', () => {
307
+ window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
308
+ });
309
+ }
310
+ }
311
+ }
312
+
302
313
  function updateDeviceBanner(service, connected) {
303
314
  state.connections[service] = connected;
304
315
  const el = $('deviceStatus');
@@ -1273,9 +1284,13 @@ function renderNetwork() {
1273
1284
  rows.appendChild(frag);
1274
1285
  }
1275
1286
 
1287
+ function _isHttpError(r) {
1288
+ return r.phase === 'error' || (r.status && r.status >= 400);
1289
+ }
1290
+
1276
1291
  function buildNetRow(r, wfMin, wfRange) {
1277
1292
  const row = document.createElement('div');
1278
- row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (r.phase === 'error' ? ' error' : '');
1293
+ row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (_isHttpError(r) ? ' error' : '');
1279
1294
  row.dataset.id = r.id;
1280
1295
 
1281
1296
  const urlObj = tryURL(r.url);
@@ -1291,7 +1306,9 @@ function buildNetRow(r, wfMin, wfRange) {
1291
1306
  const method = r.method || '?';
1292
1307
  const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
1293
1308
  const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
1294
- nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
1309
+ const isErr = _isHttpError(r);
1310
+ const pathCls = isErr ? ' net-path-error' : '';
1311
+ nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path${pathCls}" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
1295
1312
  row.appendChild(nameCell);
1296
1313
 
1297
1314
  // Status
@@ -1301,7 +1318,13 @@ function buildNetRow(r, wfMin, wfRange) {
1301
1318
  statusCell.style.width = NET_COLS[1].width + 'px';
1302
1319
  let statusStr = '...', sCls = 's-pending';
1303
1320
  if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
1304
- else if (r.status) { statusStr = String(r.status); sCls = `s-${Math.floor(r.status/100)}`; }
1321
+ else if (r.status) {
1322
+ statusStr = String(r.status);
1323
+ const group = Math.floor(r.status / 100);
1324
+ // 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error
1325
+ if (group >= 4) sCls = 's-err';
1326
+ else sCls = `s-${group}`;
1327
+ }
1305
1328
  statusCell.className += ` ${sCls}`;
1306
1329
  statusCell.textContent = statusStr;
1307
1330
  row.appendChild(statusCell);
@@ -1350,6 +1373,7 @@ function buildNetRow(r, wfMin, wfRange) {
1350
1373
  const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
1351
1374
  let barCls = 'pending';
1352
1375
  if (r.phase === 'error') barCls = 'err';
1376
+ else if (r.status && r.status >= 400) barCls = 'err';
1353
1377
  else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
1354
1378
  wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
1355
1379
  }
@@ -1416,8 +1440,12 @@ function renderNetDetailTabs(r) {
1416
1440
  }
1417
1441
 
1418
1442
  function renderNetDetailContent(r) {
1419
- const body = $('netDetailContent');
1443
+ let body = $('netDetailContent');
1420
1444
  if (!body) return;
1445
+ // Clone-replace to remove all stale event listeners (prevents contextmenu leak)
1446
+ const fresh = body.cloneNode(false);
1447
+ body.parentNode.replaceChild(fresh, body);
1448
+ body = fresh;
1421
1449
  const tab = r._tab || 'headers';
1422
1450
 
1423
1451
  if (tab === 'headers') {
@@ -1436,7 +1464,7 @@ function renderNetDetailContent(r) {
1436
1464
  <div class="kv-grid">
1437
1465
  <span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
1438
1466
  <span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
1439
- <span class="kv-key">Status</span><span class="kv-val ${r.status ? 's-' + Math.floor(r.status/100) : 's-pending'}">${r.status || 'Pending'} ${r.statusText || ''}</span>
1467
+ <span class="kv-key">Status</span><span class="kv-val ${r.phase === 'error' ? 's-err' : r.status ? (r.status >= 400 ? 's-err' : 's-' + Math.floor(r.status/100)) : 's-pending'}">${r.phase === 'error' ? (r.status || 'ERR') : (r.status || 'Pending')} ${r.statusText || (r.phase === 'error' ? r.error || 'Network Error' : '')}</span>
1440
1468
  </div>
1441
1469
  ${renderH('Response Headers', rsH)}
1442
1470
  ${renderH('Request Headers', rqH)}`;
@@ -1460,16 +1488,27 @@ function renderNetDetailContent(r) {
1460
1488
  }
1461
1489
  }
1462
1490
  } else if (tab === 'preview') {
1463
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1491
+ const isErrStatus = _isHttpError(r);
1492
+ if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1464
1493
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
1465
1494
  // Render as collapsible JSON tree with right-click copy
1466
1495
  const val = r.responseBody;
1467
1496
  let treeData = val;
1468
1497
  if (typeof val === 'string') {
1469
- try { treeData = JSON.parse(val); } catch { body.textContent = val; return; }
1498
+ try { treeData = JSON.parse(val); } catch {
1499
+ body.innerHTML = `<span style="color:${isErrStatus ? 'var(--red)' : 'inherit'}">${esc(val)}</span>`;
1500
+ return;
1501
+ }
1470
1502
  }
1471
1503
  if (treeData && typeof treeData === 'object') {
1472
1504
  body.innerHTML = '';
1505
+ // Show error status banner above the response body
1506
+ if (isErrStatus) {
1507
+ const errBanner = document.createElement('div');
1508
+ errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
1509
+ errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
1510
+ body.appendChild(errBanner);
1511
+ }
1473
1512
  body.appendChild(createTreeNode(null, treeData, false));
1474
1513
  // Right-click on preview to copy the whole object or clicked node value
1475
1514
  body.addEventListener('contextmenu', (e) => {
@@ -1477,12 +1516,27 @@ function renderNetDetailContent(r) {
1477
1516
  showPreviewCopyMenu(e, treeData);
1478
1517
  });
1479
1518
  } else {
1480
- body.innerHTML = '<span style="color:var(--text-dim)">No preview available</span>';
1519
+ body.innerHTML = isErrStatus
1520
+ ? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
1521
+ : '<span style="color:var(--text-dim)">No preview available</span>';
1481
1522
  }
1482
1523
  } else if (tab === 'response') {
1483
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1524
+ const isErrStatus = _isHttpError(r);
1525
+ if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1484
1526
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
1485
- body.innerHTML = renderJSON(r.responseBody);
1527
+ if (isErrStatus) {
1528
+ const errBanner = document.createElement('div');
1529
+ errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
1530
+ errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
1531
+ body.innerHTML = '';
1532
+ body.appendChild(errBanner);
1533
+ const raw = document.createElement('div');
1534
+ raw.style.color = 'var(--red)';
1535
+ raw.innerHTML = renderJSON(r.responseBody);
1536
+ body.appendChild(raw);
1537
+ } else {
1538
+ body.innerHTML = renderJSON(r.responseBody);
1539
+ }
1486
1540
  }
1487
1541
  }
1488
1542
 
@@ -2433,6 +2487,9 @@ function initSettingsPanel() {
2433
2487
  setStoredFontSize(size);
2434
2488
  applyFontSize(size);
2435
2489
  });
2490
+
2491
+ // Apply update banner if update info arrived before settings panel was created
2492
+ _applyUpdateBanner();
2436
2493
  }
2437
2494
 
2438
2495
  // Apply saved theme + font size + app name on load
package/main.js CHANGED
@@ -19,6 +19,15 @@ const PORTS = {
19
19
  let mainWindow = null;
20
20
  let devtoolsWindow = null; // hosts the embedded CDP DevTools frontend
21
21
 
22
+ // Safe IPC send — prevents "Object has been destroyed" crash
23
+ function _send(channel, ...args) {
24
+ try {
25
+ if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
26
+ mainWindow.webContents.send(channel, ...args);
27
+ }
28
+ } catch {}
29
+ }
30
+
22
31
  // ─── State ────────────────────────────────────────────────────────────────────
23
32
  let reduxClients = new Set();
24
33
  let storageClients = new Set();
@@ -27,8 +36,32 @@ let networkClients = new Set();
27
36
  // ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
28
37
  const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
29
38
 
39
+ // ─── Single Instance Lock ────────────────────────────────────────────────────
40
+ // Prevent multiple ReactoRadar instances from running simultaneously.
41
+ // If a second instance launches, focus the existing window instead.
42
+ const gotLock = app.requestSingleInstanceLock();
43
+ if (!gotLock) {
44
+ // Another instance is already running — show a dialog and quit
45
+ const { dialog } = require('electron');
46
+ app.whenReady().then(() => {
47
+ dialog.showErrorBox(
48
+ 'ReactoRadar is already running',
49
+ 'Another instance of ReactoRadar is already open.\n\nPlease close the existing instance first, or check your system tray / dock.\n\nIf the old version is stuck, run:\n kill $(lsof -ti :9092) \nin your terminal to stop it.'
50
+ );
51
+ app.quit();
52
+ });
53
+ } else {
54
+ app.on('second-instance', () => {
55
+ // Focus the existing window when someone tries to open a second instance
56
+ if (mainWindow) {
57
+ if (mainWindow.isMinimized()) mainWindow.restore();
58
+ mainWindow.focus();
59
+ }
60
+ });
61
+ }
62
+
30
63
  // ─── App lifecycle ────────────────────────────────────────────────────────────
31
- app.whenReady().then(async () => {
64
+ if (gotLock) app.whenReady().then(async () => {
32
65
  nativeTheme.themeSource = 'dark';
33
66
 
34
67
  // Set dock icon on macOS
@@ -42,11 +75,11 @@ app.whenReady().then(async () => {
42
75
  let appVersion;
43
76
  try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
44
77
  // Send multiple times to ensure renderer catches it
45
- mainWindow?.webContents.on('did-finish-load', () => {
78
+ mainWindow.webContents.on('did-finish-load', () => {
46
79
  [200, 1000, 3000].forEach(delay => {
47
80
  setTimeout(() => {
48
81
  if (mainWindow && !mainWindow.isDestroyed()) {
49
- mainWindow.webContents.send('app-version', appVersion);
82
+ _send('app-version', appVersion);
50
83
  }
51
84
  }, delay);
52
85
  });
@@ -102,11 +135,23 @@ async function createMainWindow() {
102
135
 
103
136
  // Open the JS Debugger panel (CDP DevTools) in a second window
104
137
  mainWindow.webContents.on('did-finish-load', () => {
105
- mainWindow.webContents.send('ports', PORTS);
138
+ _send('ports', PORTS);
106
139
  });
107
140
  }
108
141
 
109
142
  // ─── Update Checker ──────────────────────────────────────────────────────────
143
+ function _semverCompare(a, b) {
144
+ // Returns 1 if a > b, -1 if a < b, 0 if equal
145
+ const pa = (a || '').split('.').map(Number);
146
+ const pb = (b || '').split('.').map(Number);
147
+ for (let i = 0; i < 3; i++) {
148
+ const va = pa[i] || 0, vb = pb[i] || 0;
149
+ if (va > vb) return 1;
150
+ if (va < vb) return -1;
151
+ }
152
+ return 0;
153
+ }
154
+
110
155
  function checkForUpdates() {
111
156
  const currentVersion = require('./package.json').version;
112
157
  https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
@@ -115,9 +160,16 @@ function checkForUpdates() {
115
160
  res.on('end', () => {
116
161
  try {
117
162
  const latest = JSON.parse(data).version;
118
- if (latest && latest !== currentVersion) {
119
- // Notify the renderer to show an update banner
120
- mainWindow?.webContents.send('update-available', { current: currentVersion, latest });
163
+ if (latest && _semverCompare(latest, currentVersion) > 0) {
164
+ // Send with retries to ensure renderer catches it after did-finish-load
165
+ const payload = { current: currentVersion, latest };
166
+ [500, 2000, 5000].forEach(delay => {
167
+ setTimeout(() => {
168
+ if (mainWindow && !mainWindow.isDestroyed()) {
169
+ _send('update-available', payload);
170
+ }
171
+ }, delay);
172
+ });
121
173
  console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
122
174
  }
123
175
  } catch {}
@@ -184,7 +236,7 @@ function fetchCDPTargets(callback) {
184
236
  t.type === 'node' || t.devtoolsFrontendUrl
185
237
  );
186
238
  lastKnownTargets = rnTargets;
187
- mainWindow?.webContents.send('cdp-targets', rnTargets);
239
+ _send('cdp-targets', rnTargets);
188
240
  if (callback) callback(rnTargets);
189
241
  } catch (_) {
190
242
  if (callback) callback([]);
@@ -192,7 +244,7 @@ function fetchCDPTargets(callback) {
192
244
  });
193
245
  }).on('error', () => {
194
246
  lastKnownTargets = [];
195
- mainWindow?.webContents.send('cdp-targets', []);
247
+ _send('cdp-targets', []);
196
248
  if (callback) callback([]);
197
249
  });
198
250
  }
@@ -216,13 +268,13 @@ function startReactDevToolsServer() {
216
268
  reactDTServer.on('error', (err) => {
217
269
  console.warn(`[ReactDT] Server error: ${err.message}`);
218
270
  if (err.code === 'EADDRINUSE') {
219
- mainWindow?.webContents.send('react-dt-status', false);
271
+ _send('react-dt-status', false);
220
272
  }
221
273
  });
222
274
  reactDTServer.on('connection', (ws) => {
223
275
  reactDTClients.add(ws);
224
276
  console.log(`[ReactDT] Client connected (total: ${reactDTClients.size})`);
225
- mainWindow?.webContents.send('react-dt-status', true);
277
+ _send('react-dt-status', true);
226
278
 
227
279
  // Relay messages between all connected clients (frontend ↔ backend)
228
280
  ws.on('message', (data) => {
@@ -237,7 +289,7 @@ function startReactDevToolsServer() {
237
289
  reactDTClients.delete(ws);
238
290
  console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
239
291
  if (reactDTClients.size === 0) {
240
- mainWindow?.webContents.send('react-dt-status', false);
292
+ _send('react-dt-status', false);
241
293
  }
242
294
  });
243
295
  });
@@ -251,53 +303,67 @@ function startReactDevToolsServer() {
251
303
  function startBridgeServers() {
252
304
  // Redux Bridge
253
305
  startBridge(PORTS.REDUX_BRIDGE, 'redux', reduxClients, (event) => {
254
- mainWindow?.webContents.send('redux-event', event);
306
+ _send('redux-event', event);
255
307
  });
256
308
 
257
309
  // AsyncStorage Bridge
258
310
  startBridge(PORTS.STORAGE_BRIDGE, 'storage', storageClients, (event) => {
259
- mainWindow?.webContents.send('storage-event', event);
311
+ _send('storage-event', event);
260
312
  });
261
313
 
262
314
  // Network + Console + Perf Bridge (port 9092 carries all types from RNDebugSDK)
263
315
  startBridge(PORTS.NETWORK_BRIDGE, 'network', networkClients, (event) => {
264
316
  if (event.type === 'control') return;
265
317
  if (event.type === 'console') {
266
- mainWindow?.webContents.send('console-event', event);
318
+ _send('console-event', event);
267
319
  } else if (event.type === 'perf') {
268
- mainWindow?.webContents.send('perf-event', event);
320
+ _send('perf-event', event);
269
321
  } else if (event.type === 'ga4') {
270
- mainWindow?.webContents.send('ga4-event', event);
322
+ _send('ga4-event', event);
271
323
  } else {
272
- mainWindow?.webContents.send('network-event', event);
324
+ _send('network-event', event);
273
325
  }
274
326
  });
275
327
  }
276
328
 
277
329
  function startBridge(port, name, clients, onEvent) {
278
- const wss = new WebSocketServer({ port });
279
- wss.on('connection', (ws) => {
280
- clients.add(ws);
281
- console.log(`[${name}] RN app connected`);
282
- mainWindow?.webContents.send(`${name}-connected`, true);
283
-
284
- ws.on('message', (raw) => {
285
- try {
286
- const event = JSON.parse(raw.toString());
287
- onEvent(event);
288
- } catch (e) {
289
- console.warn(`[${name}] Failed to parse message:`, e.message);
330
+ try {
331
+ const wss = new WebSocketServer({ port });
332
+ wss.on('error', (err) => {
333
+ if (err.code === 'EADDRINUSE') {
334
+ console.error(`[${name}] Port ${port} is already in use — another ReactoRadar or debugger may be running.`);
335
+ const { dialog } = require('electron');
336
+ dialog.showErrorBox(
337
+ `Port ${port} is in use`,
338
+ `ReactoRadar cannot start the ${name} bridge because port ${port} is already occupied.\n\nThis usually means an older version of ReactoRadar is still running.\n\nTo fix this, run the following in your terminal:\n kill $(lsof -ti :${port})\n\nThen restart ReactoRadar.`
339
+ );
290
340
  }
291
341
  });
342
+ wss.on('connection', (ws) => {
343
+ clients.add(ws);
344
+ console.log(`[${name}] RN app connected`);
345
+ _send(`${name}-connected`, true);
292
346
 
293
- ws.on('close', () => {
294
- clients.delete(ws);
295
- if (clients.size === 0) {
296
- mainWindow?.webContents.send(`${name}-connected`, false);
297
- }
347
+ ws.on('message', (raw) => {
348
+ try {
349
+ const event = JSON.parse(raw.toString());
350
+ onEvent(event);
351
+ } catch (e) {
352
+ console.warn(`[${name}] Failed to parse message:`, e.message);
353
+ }
354
+ });
355
+
356
+ ws.on('close', () => {
357
+ clients.delete(ws);
358
+ if (clients.size === 0) {
359
+ _send(`${name}-connected`, false);
360
+ }
361
+ });
298
362
  });
299
- });
300
- console.log(`[${name}] Bridge on :${port}`);
363
+ console.log(`[${name}] Bridge on :${port}`);
364
+ } catch (e) {
365
+ console.error(`[${name}] Failed to start bridge on port ${port}:`, e.message);
366
+ }
301
367
  }
302
368
 
303
369
  // ─── IPC from Renderer ────────────────────────────────────────────────────────
@@ -336,9 +402,11 @@ function setupIPC() {
336
402
  // clear-all is handled by renderer via clear-all-ui IPC from menu
337
403
 
338
404
  ipcMain.on('set-metro-port', (_, port) => {
339
- PORTS.METRO = port;
405
+ const p = parseInt(port);
406
+ if (isNaN(p) || p < 1024 || p > 65535) return;
407
+ PORTS.METRO = p;
340
408
  fetchCDPTargets();
341
- mainWindow?.webContents.send('ports', PORTS);
409
+ _send('ports', PORTS);
342
410
  });
343
411
 
344
412
  ipcMain.on('set-network-capture', (_, enabled) => {
@@ -482,7 +550,7 @@ function buildMenu() {
482
550
  {
483
551
  label: 'Open JS Debugger (CDP)',
484
552
  accelerator: 'Cmd+D',
485
- click: () => { mainWindow?.webContents.send('trigger-open-cdp'); },
553
+ click: () => { _send('trigger-open-cdp'); },
486
554
  },
487
555
  {
488
556
  label: 'Open React DevTools',
@@ -493,7 +561,7 @@ function buildMenu() {
493
561
  {
494
562
  label: 'Clear All',
495
563
  accelerator: 'Cmd+K',
496
- click: () => { mainWindow?.webContents.send('clear-all-ui'); },
564
+ click: () => { _send('clear-all-ui'); },
497
565
  },
498
566
  { type: 'separator' },
499
567
  {
@@ -506,7 +574,7 @@ function buildMenu() {
506
574
  const next = themes[(idx + 1) % themes.length];
507
575
  nativeTheme.themeSource = next.includes('light') ? 'light' : 'dark';
508
576
  if (mainWindow && !mainWindow.isDestroyed()) {
509
- mainWindow.webContents.send('theme-changed', next);
577
+ _send('theme-changed', next);
510
578
  }
511
579
  },
512
580
  },
@@ -526,7 +594,7 @@ function buildMenu() {
526
594
  {
527
595
  label: 'Find',
528
596
  accelerator: 'Cmd+F',
529
- click: () => { mainWindow?.webContents.send('focus-search'); },
597
+ click: () => { _send('focus-search'); },
530
598
  },
531
599
  ],
532
600
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.3",
4
+ "version": "1.5.5",
5
5
  "description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
6
6
  "main": "main.js",
7
7
  "bin": {
package/styles.css CHANGED
@@ -741,7 +741,11 @@ body {
741
741
  }
742
742
  .net-row:hover { background: var(--bg3); }
743
743
  .net-row.selected { background: var(--bg4); }
744
- .net-row.error { background: rgba(255,94,114,.04); }
744
+ .net-row.error { background: rgba(255,94,114,.06); border-bottom-color: rgba(255,94,114,.10); }
745
+ .net-row.error:hover { background: rgba(255,94,114,.10); }
746
+ .net-row.error .net-path { color: var(--red); }
747
+ .net-row.error .net-host { color: rgba(255,94,114,.6); }
748
+ .net-path-error { color: var(--red) !important; }
745
749
  .net-cell {
746
750
  padding: 6px 8px;
747
751
  overflow: hidden;
@@ -757,12 +761,13 @@ body {
757
761
  .net-cell-name .method-badge { flex-shrink: 0; vertical-align: middle; }
758
762
 
759
763
  .net-status { font-weight: 700; }
760
- .s-2 { color: var(--green); }
761
- .s-3 { color: var(--yellow); }
762
- .s-4 { color: var(--orange); }
763
- .s-5 { color: var(--red); }
764
+ .s-1 { color: var(--text-mid); } /* 1xx informational */
765
+ .s-2 { color: var(--green); } /* 2xx success */
766
+ .s-3 { color: var(--yellow); } /* 3xx redirect */
767
+ .s-4 { color: var(--red); } /* 4xx client error */
768
+ .s-5 { color: var(--red); } /* 5xx server error */
764
769
  .s-pending { color: var(--text-dim); }
765
- .s-err { color: var(--red); }
770
+ .s-err { color: var(--red); font-weight: 700; }
766
771
 
767
772
  .net-type { color: var(--text-dim); font-size: 10px; }
768
773
  .net-initiator { color: var(--text-dim); font-size: 10px; }