reactoradar 1.5.3 → 1.5.4

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 +25 -4
  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
@@ -107,6 +107,18 @@ async function createMainWindow() {
107
107
  }
108
108
 
109
109
  // ─── Update Checker ──────────────────────────────────────────────────────────
110
+ function _semverCompare(a, b) {
111
+ // Returns 1 if a > b, -1 if a < b, 0 if equal
112
+ const pa = (a || '').split('.').map(Number);
113
+ const pb = (b || '').split('.').map(Number);
114
+ for (let i = 0; i < 3; i++) {
115
+ const va = pa[i] || 0, vb = pb[i] || 0;
116
+ if (va > vb) return 1;
117
+ if (va < vb) return -1;
118
+ }
119
+ return 0;
120
+ }
121
+
110
122
  function checkForUpdates() {
111
123
  const currentVersion = require('./package.json').version;
112
124
  https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
@@ -115,9 +127,16 @@ function checkForUpdates() {
115
127
  res.on('end', () => {
116
128
  try {
117
129
  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 });
130
+ if (latest && _semverCompare(latest, currentVersion) > 0) {
131
+ // Send with retries to ensure renderer catches it after did-finish-load
132
+ const payload = { current: currentVersion, latest };
133
+ [500, 2000, 5000].forEach(delay => {
134
+ setTimeout(() => {
135
+ if (mainWindow && !mainWindow.isDestroyed()) {
136
+ mainWindow.webContents.send('update-available', payload);
137
+ }
138
+ }, delay);
139
+ });
121
140
  console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
122
141
  }
123
142
  } catch {}
@@ -336,7 +355,9 @@ function setupIPC() {
336
355
  // clear-all is handled by renderer via clear-all-ui IPC from menu
337
356
 
338
357
  ipcMain.on('set-metro-port', (_, port) => {
339
- PORTS.METRO = port;
358
+ const p = parseInt(port);
359
+ if (isNaN(p) || p < 1024 || p > 65535) return;
360
+ PORTS.METRO = p;
340
361
  fetchCDPTargets();
341
362
  mainWindow?.webContents.send('ports', PORTS);
342
363
  });
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.4",
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; }