reactoradar 1.6.4 → 1.6.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.
package/app.js CHANGED
@@ -194,23 +194,37 @@ function clearActiveTab() {
194
194
  const memHT = $('memHeapTotal'); if (memHT) memHT.textContent = '—';
195
195
  const memN = $('memNative'); if (memN) memN.textContent = '—';
196
196
  break;
197
+ case 'native':
198
+ _nativeState.logs = [];
199
+ if ($('nativeBadge')) $('nativeBadge').textContent = '0';
200
+ const nativeList = $('nativeLogList');
201
+ if (nativeList) nativeList.innerHTML = '';
202
+ break;
197
203
  default:
198
204
  break;
199
205
  }
200
206
  }
201
207
 
202
- // Clear all (used by IPC clear-all-ui from menu Cmd+K)
208
+ // Clear all (used by IPC clear-all-ui from menu Cmd+K, and on device disconnect)
203
209
  function clearAll() {
210
+ // Cancel pending render batches
211
+ if (_consoleRAF) { cancelAnimationFrame(_consoleRAF); _consoleRAF = null; }
212
+ if (_netRAF) { cancelAnimationFrame(_netRAF); _netRAF = null; }
213
+ if (_storageRAF) { cancelAnimationFrame(_storageRAF); _storageRAF = null; }
214
+ // Console
204
215
  state.console.logs = [];
205
216
  _consolePending = [];
206
217
  _lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
218
+ // Network
207
219
  state.network.requests = {};
208
220
  state.network.order = [];
209
221
  state.network.selectedId = null;
210
222
  closeNetDetail();
223
+ // Redux
211
224
  state.redux.actions = [];
212
225
  state.redux.states = [];
213
226
  state.redux.selected = -1;
227
+ // Storage
214
228
  state.storage.entries = {};
215
229
  state.storage.keys = [];
216
230
  state.storage.selected = null;
@@ -226,6 +240,21 @@ function clearAll() {
226
240
  _nativeState.logs = [];
227
241
  const nativeList = $('nativeLogList');
228
242
  if (nativeList) nativeList.innerHTML = '';
243
+ // Performance
244
+ perfState.fps = [];
245
+ perfState.jsThread = [];
246
+ perfState.uiThread = [];
247
+ perfState.data = [];
248
+ const perfFPS = $('perfFPS'); if (perfFPS) perfFPS.textContent = '—';
249
+ const perfJS = $('perfJS'); if (perfJS) perfJS.textContent = '—';
250
+ const perfUI = $('perfUI'); if (perfUI) perfUI.textContent = '—';
251
+ clearPerfCanvas('perfFPSCanvas');
252
+ clearPerfCanvas('perfJSCanvas');
253
+ clearPerfCanvas('perfUICanvas');
254
+ // Memory
255
+ const memHU = $('memHeapUsed'); if (memHU) memHU.textContent = '—';
256
+ const memHT = $('memHeapTotal'); if (memHT) memHT.textContent = '—';
257
+ const memN = $('memNative'); if (memN) memN.textContent = '—';
229
258
  // Badges
230
259
  $('cBadge').textContent = '0';
231
260
  $('nBadge').textContent = '0';
@@ -241,6 +270,42 @@ function clearAll() {
241
270
  if (typeof renderGA4List === 'function') { renderGA4List(); renderGA4Summary(); }
242
271
  }
243
272
 
273
+ // Free heavy in-memory data without clearing the visible UI.
274
+ // Called on device disconnect and app quit to reduce memory footprint
275
+ // while keeping logs/network/redux visible for inspection.
276
+ function freeMemory() {
277
+ // Drop response/request bodies from network requests (biggest memory hog)
278
+ for (const id of state.network.order) {
279
+ const r = state.network.requests[id];
280
+ if (r) { r.responseBody = null; r.requestBody = null; }
281
+ }
282
+ // Trim console logs to a small tail (keep last 200 for reference)
283
+ if (state.console.logs.length > 200) {
284
+ state.console.logs = state.console.logs.slice(-200);
285
+ }
286
+ // Drop full Redux state snapshots (keep action metadata)
287
+ state.redux.states = [];
288
+ // Drop storage values (keep keys for reference)
289
+ for (const k in state.storage.entries) {
290
+ state.storage.entries[k] = null;
291
+ }
292
+ // Trim GA4 events
293
+ if (ga4State.events.length > 200) {
294
+ ga4State.events = ga4State.events.slice(-200);
295
+ }
296
+ // Trim native logs
297
+ if (_nativeState.logs.length > 200) {
298
+ _nativeState.logs = _nativeState.logs.slice(-200);
299
+ }
300
+ // Drop performance timeline data
301
+ perfState.data = [];
302
+ perfState.fps = [];
303
+ perfState.jsThread = [];
304
+ perfState.uiThread = [];
305
+ // Flush pending console batch
306
+ _consolePending = [];
307
+ }
308
+
244
309
  // ─── CDP Button ───────────────────────────────────────────────────────────────
245
310
  $('btnCDP')?.addEventListener('click', () => {
246
311
  // Tell main process to open the CDP DevTools window with the best available target
@@ -294,13 +359,25 @@ if (window.electronAPI) {
294
359
  handleMemoryEvent(event);
295
360
  });
296
361
 
297
- window.electronAPI.on('redux-connected', on => { updateDeviceBanner('redux', on); });
298
- window.electronAPI.on('network-connected', on => { updateDeviceBanner('network', on); });
299
- window.electronAPI.on('storage-connected', on => { updateDeviceBanner('storage', on); });
300
- window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
301
-
302
362
  window.electronAPI.on('clear-all-ui', clearAll);
303
363
 
364
+ // When all device bridges disconnect, release heavy memory but keep logs visible.
365
+ // Debounced to avoid data loss during hot reloads or flaky connections.
366
+ let _disconnectTimer = null;
367
+ window.electronAPI.on('device-all-disconnected', () => {
368
+ clearTimeout(_disconnectTimer);
369
+ _disconnectTimer = setTimeout(() => {
370
+ console.log('[App] All devices disconnected — freeing memory');
371
+ freeMemory();
372
+ }, 3000);
373
+ });
374
+ // Cancel pending free if a device reconnects
375
+ const _cancelDisconnectTimer = () => { clearTimeout(_disconnectTimer); _disconnectTimer = null; };
376
+ window.electronAPI.on('redux-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('redux', on); });
377
+ window.electronAPI.on('network-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('network', on); });
378
+ window.electronAPI.on('storage-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('storage', on); });
379
+ window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
380
+
304
381
  // Cmd+F — focus the search input for the active panel
305
382
  function _handleFind() {
306
383
  // If network detail is open, focus the detail search
@@ -341,8 +418,9 @@ if (window.electronAPI) {
341
418
  }
342
419
  });
343
420
 
344
- window.electronAPI.on('app-version', (version) => {
421
+ window.electronAPI.on('app-version', (version, isPackaged) => {
345
422
  state._appVersion = version;
423
+ state._isPackaged = !!isPackaged;
346
424
  // Update anywhere the version is displayed
347
425
  document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
348
426
  });
@@ -426,16 +504,19 @@ function _applyUpdateBanner() {
426
504
  }
427
505
 
428
506
  async function _showChangelog(version) {
507
+ if (!version || typeof version !== 'string') return;
508
+
429
509
  // Remove existing modal
430
510
  $('changelogModal')?.remove();
431
511
 
512
+ const safeVersion = esc(version);
432
513
  const modal = document.createElement('div');
433
514
  modal.id = 'changelogModal';
434
515
  modal.className = 'changelog-modal-overlay';
435
516
  modal.innerHTML = `
436
517
  <div class="changelog-modal">
437
518
  <div class="changelog-header">
438
- <span class="changelog-title">What's New in v${esc(version)}</span>
519
+ <span class="changelog-title">What's New in v${safeVersion}</span>
439
520
  <button class="changelog-close" id="changelogClose">&times;</button>
440
521
  </div>
441
522
  <div class="changelog-body" id="changelogBody">
@@ -452,6 +533,11 @@ async function _showChangelog(version) {
452
533
  try {
453
534
  const notes = await window.electronAPI?.fetchChangelog(version);
454
535
  const body = $('changelogBody');
536
+ if (!body) return;
537
+ if (!notes || typeof notes !== 'string') {
538
+ body.innerHTML = '<div style="color:var(--text-dim);padding:20px;text-align:center">No release notes available.</div>';
539
+ return;
540
+ }
455
541
  if (body && notes) {
456
542
  // Simple markdown-like rendering
457
543
  body.innerHTML = notes
@@ -814,6 +900,41 @@ window.electronAPI?.on('console-event', addConsoleLog);
814
900
  // ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
815
901
  // Builds interactive, collapsible DOM nodes for objects/arrays.
816
902
 
903
+ // Collect all entries for an object: own data properties + prototype getter values.
904
+ // Getter-derived keys use the clean name (e.g. "deliveryId") and skip backing
905
+ // fields (e.g. "_deliveryId") so the log output mirrors the model's public API.
906
+ function collectEntries(val) {
907
+ if (Array.isArray(val)) return val.map((v, i) => [i, v]);
908
+
909
+ const result = {};
910
+ const getterKeys = new Set();
911
+
912
+ // 1. Walk prototype chain and invoke getters
913
+ let proto = Object.getPrototypeOf(val);
914
+ while (proto && proto !== Object.prototype) {
915
+ const descs = Object.getOwnPropertyDescriptors(proto);
916
+ for (const [k, desc] of Object.entries(descs)) {
917
+ if (k === 'constructor') continue;
918
+ if (desc.get && !(k in result)) {
919
+ try { result[k] = desc.get.call(val); } catch { /* skip broken getters */ }
920
+ getterKeys.add(k);
921
+ }
922
+ }
923
+ proto = Object.getPrototypeOf(proto);
924
+ }
925
+
926
+ // 2. Add own data properties, but skip backing fields whose getter is present.
927
+ // Convention: getter "foo" backs "_foo"; if "foo" was collected, skip "_foo".
928
+ const ownKeys = Object.keys(val);
929
+ for (const k of ownKeys) {
930
+ const clean = k.startsWith('_') ? k.slice(1) : null;
931
+ if (clean && getterKeys.has(clean)) continue; // skip _backing field
932
+ if (!(k in result)) result[k] = val[k];
933
+ }
934
+
935
+ return Object.entries(result);
936
+ }
937
+
817
938
  function objPreview(val, maxLen) {
818
939
  maxLen = maxLen || 80;
819
940
  if (val === null) return 'null';
@@ -831,16 +952,16 @@ function objPreview(val, maxLen) {
831
952
  return `(${val.length}) [${items.join(', ')}${suffix}]`;
832
953
  }
833
954
  if (typeof val === 'object') {
834
- const keys = Object.keys(val);
835
- if (keys.length === 0) return '{}';
955
+ const entries = collectEntries(val);
956
+ if (entries.length === 0) return '{}';
836
957
  const items = [];
837
958
  let len = 2;
838
- for (let i = 0; i < keys.length && len < maxLen; i++) {
839
- const s = `${keys[i]}: ${primitivePreview(val[keys[i]])}`;
959
+ for (let i = 0; i < entries.length && len < maxLen; i++) {
960
+ const s = `${entries[i][0]}: ${primitivePreview(entries[i][1])}`;
840
961
  len += s.length + 2;
841
962
  items.push(s);
842
963
  }
843
- const suffix = items.length < keys.length ? ', ...' : '';
964
+ const suffix = items.length < entries.length ? ', ...' : '';
844
965
  return `{${items.join(', ')}${suffix}}`;
845
966
  }
846
967
  return primitivePreview(val);
@@ -909,7 +1030,7 @@ function createTreeNode(key, val, startCollapsed) {
909
1030
  function populateChildren() {
910
1031
  if (populated) return;
911
1032
  populated = true;
912
- const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val);
1033
+ const entries = collectEntries(val);
913
1034
  entries.forEach(([k, v]) => {
914
1035
  children.appendChild(createTreeNode(k, v, true));
915
1036
  });
@@ -3247,10 +3368,18 @@ function initNativeLogsPanel() {
3247
3368
  <div class="native-log-list" id="nativeLogList"></div>
3248
3369
  </div>`;
3249
3370
 
3250
- // Connect buttons
3251
- $('nativeConnectAndroid')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('android'));
3252
- $('nativeConnectIOSSim')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('ios-sim'));
3253
- $('nativeConnectIOSDevice')?.addEventListener('click', () => window.electronAPI?.startNativeLogs('ios-device'));
3371
+ // Connect buttons — auto-enable tab when user clicks connect
3372
+ function _enableNativeTab() {
3373
+ const vis = getTabVisibility();
3374
+ if (!vis['native']) {
3375
+ vis['native'] = true;
3376
+ setTabVisibility(vis);
3377
+ applyTabVisibility();
3378
+ }
3379
+ }
3380
+ $('nativeConnectAndroid')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('android'); });
3381
+ $('nativeConnectIOSSim')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-sim'); });
3382
+ $('nativeConnectIOSDevice')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-device'); });
3254
3383
  $('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
3255
3384
 
3256
3385
  // Clear buttons (toolbar + logs area)
@@ -3860,6 +3989,27 @@ function initSettingsPanel() {
3860
3989
  <b style="color:var(--text)">5.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar remove</code> to uninstall
3861
3990
  </div>
3862
3991
  </div>
3992
+ <div class="settings-section">
3993
+ <div class="settings-section-title">Version History</div>
3994
+ <div class="settings-hint" style="margin-bottom:4px">Roll back to a previous version if you notice issues.</div>
3995
+ <div class="settings-hint rollback-steps" id="rollbackSteps" style="margin-bottom:10px;line-height:1.8;font-size:10px">
3996
+ <b style="color:var(--text)">How to roll back:</b><br/>
3997
+ <span id="rollbackDmgSteps" style="display:none">
3998
+ <b style="color:var(--text)">1.</b> Click <b>Download</b> on the version you want<br/>
3999
+ <b style="color:var(--text)">2.</b> Open the downloaded <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">.dmg</code> file<br/>
4000
+ <b style="color:var(--text)">3.</b> Drag the app to Applications (replace existing)<br/>
4001
+ <b style="color:var(--text)">4.</b> Relaunch ReactoRadar
4002
+ </span>
4003
+ <span id="rollbackNpmSteps" style="display:none">
4004
+ <b style="color:var(--text)">1.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@&lt;version&gt;</code> e.g. <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@1.6.4</code><br/>
4005
+ <b style="color:var(--text)">2.</b> Or pin globally: <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npm i -g reactoradar@1.6.4</code><br/>
4006
+ <b style="color:var(--text)">3.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">reactoradar</code> to launch
4007
+ </span>
4008
+ </div>
4009
+ <div id="versionHistoryList" class="version-history-list">
4010
+ <div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Loading versions...</div>
4011
+ </div>
4012
+ </div>
3863
4013
  </div>
3864
4014
  </div>
3865
4015
  </div>`;
@@ -3966,6 +4116,120 @@ function initSettingsPanel() {
3966
4116
 
3967
4117
  // Apply update banner if update info arrived before settings panel was created
3968
4118
  _applyUpdateBanner();
4119
+
4120
+ // Fetch and render version history for rollback
4121
+ _loadVersionHistory();
4122
+ }
4123
+
4124
+ function _loadVersionHistory() {
4125
+ const container = $('versionHistoryList');
4126
+ if (!container) return;
4127
+ if (!window.electronAPI || typeof window.electronAPI.fetchReleases !== 'function') {
4128
+ container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Version history not available.</div>';
4129
+ return;
4130
+ }
4131
+
4132
+ // Show appropriate rollback steps based on install type
4133
+ const isPackaged = !!state._isPackaged;
4134
+ const dmgSteps = $('rollbackDmgSteps');
4135
+ const npmSteps = $('rollbackNpmSteps');
4136
+ if (dmgSteps) dmgSteps.style.display = isPackaged ? '' : 'none';
4137
+ if (npmSteps) npmSteps.style.display = isPackaged ? 'none' : '';
4138
+
4139
+ window.electronAPI.fetchReleases().then(releases => {
4140
+ if (!Array.isArray(releases) || releases.length === 0) {
4141
+ container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions.</div>';
4142
+ return;
4143
+ }
4144
+
4145
+ const currentVersion = state._appVersion || '';
4146
+ container.innerHTML = '';
4147
+
4148
+ releases.forEach(r => {
4149
+ if (!r || !r.version) return; // skip malformed entries
4150
+
4151
+ const isCurrent = r.version === currentVersion;
4152
+ const row = document.createElement('div');
4153
+ row.className = 'version-row' + (isCurrent ? ' version-current' : '');
4154
+
4155
+ // Safe date formatting
4156
+ let dateStr = '';
4157
+ if (r.date) {
4158
+ try {
4159
+ const d = new Date(r.date);
4160
+ if (!isNaN(d.getTime())) {
4161
+ dateStr = d.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' });
4162
+ }
4163
+ } catch { /* skip bad date */ }
4164
+ }
4165
+
4166
+ // Build action button based on install type
4167
+ let actionHtml = '';
4168
+ if (isCurrent) {
4169
+ actionHtml = '<span class="version-installed">Installed</span>';
4170
+ } else if (isPackaged) {
4171
+ actionHtml = '<button class="version-install-btn" title="Download .dmg for this version">Download</button>';
4172
+ } else {
4173
+ actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
4174
+ }
4175
+
4176
+ row.innerHTML = `
4177
+ <div class="version-info">
4178
+ <span class="version-tag">v${esc(r.version)}${r.prerelease ? ' <span class="version-pre">pre</span>' : ''}${isCurrent ? ' <span class="version-badge">current</span>' : ''}</span>
4179
+ <span class="version-date">${esc(dateStr)}</span>
4180
+ </div>
4181
+ <div class="version-actions">
4182
+ ${actionHtml}
4183
+ <button class="version-notes-btn" title="View release notes">Notes</button>
4184
+ </div>`;
4185
+
4186
+ // DMG download button — opens the .dmg asset or release page
4187
+ const installBtn = row.querySelector('.version-install-btn');
4188
+ if (installBtn) {
4189
+ installBtn.addEventListener('click', () => {
4190
+ const url = r.dmgUrl || r.htmlUrl || '';
4191
+ if (url) {
4192
+ window.electronAPI.openExternal(url);
4193
+ }
4194
+ });
4195
+ }
4196
+
4197
+ // NPM copy button — copies the npx command to clipboard
4198
+ const npmBtn = row.querySelector('.version-npm-btn');
4199
+ if (npmBtn) {
4200
+ npmBtn.addEventListener('click', () => {
4201
+ const cmd = `npx reactoradar@${r.version}`;
4202
+ navigator.clipboard.writeText(cmd).then(() => {
4203
+ const orig = npmBtn.textContent;
4204
+ npmBtn.textContent = 'Copied!';
4205
+ npmBtn.style.color = 'var(--green)';
4206
+ setTimeout(() => { npmBtn.textContent = orig; npmBtn.style.color = ''; }, 2000);
4207
+ }).catch(() => {});
4208
+ });
4209
+ }
4210
+
4211
+ // Notes button — show changelog in modal
4212
+ const notesBtn = row.querySelector('.version-notes-btn');
4213
+ if (notesBtn) {
4214
+ notesBtn.addEventListener('click', () => {
4215
+ if (r.version && typeof _showChangelog === 'function') {
4216
+ _showChangelog(r.version);
4217
+ }
4218
+ });
4219
+ }
4220
+
4221
+ container.appendChild(row);
4222
+ });
4223
+
4224
+ // If no rows were rendered (all entries were malformed)
4225
+ if (container.children.length === 0) {
4226
+ container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">No versions found.</div>';
4227
+ }
4228
+ }).catch(() => {
4229
+ if (container) {
4230
+ container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions. Check your internet connection.</div>';
4231
+ }
4232
+ });
3969
4233
  }
3970
4234
 
3971
4235
  // ─── Memory Monitor ──────────────────────────────────────────────────────────
package/main.js CHANGED
@@ -35,6 +35,7 @@ function _send(channel, ...args) {
35
35
  let reduxClients = new Set();
36
36
  let storageClients = new Set();
37
37
  let networkClients = new Set();
38
+ const _bridgeServers = []; // track bridge WSS instances for cleanup on quit
38
39
 
39
40
  // ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
40
41
  const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
@@ -74,15 +75,16 @@ if (gotLock) app.whenReady().then(async () => {
74
75
 
75
76
  await createMainWindow();
76
77
 
77
- // Send version to renderer — try package.json, fallback to app.getVersion()
78
+ // Send version + install type to renderer — try package.json, fallback to app.getVersion()
78
79
  let appVersion;
79
80
  try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
81
+ const isPackaged = app.isPackaged;
80
82
  // Send multiple times to ensure renderer catches it (covers race conditions)
81
83
  mainWindow.webContents.on('did-finish-load', () => {
82
84
  // Send immediately + retries
83
- _send('app-version', appVersion);
85
+ _send('app-version', appVersion, isPackaged);
84
86
  [500, 2000, 5000].forEach(delay => {
85
- setTimeout(() => _send('app-version', appVersion), delay);
87
+ setTimeout(() => _send('app-version', appVersion, isPackaged), delay);
86
88
  });
87
89
  });
88
90
 
@@ -102,12 +104,27 @@ app.on('window-all-closed', () => {
102
104
 
103
105
  app.on('before-quit', () => {
104
106
  _forceQuit = true;
107
+ // Free renderer memory before shutdown (logs are not cleared — user may still see them briefly)
108
+ _send('device-all-disconnected');
109
+ // Close CDP DevTools window if open
110
+ if (devtoolsWindow && !devtoolsWindow.isDestroyed()) {
111
+ devtoolsWindow.destroy();
112
+ devtoolsWindow = null;
113
+ }
105
114
  // Close all WS servers gracefully
106
115
  if (reactDTServer) {
107
116
  reactDTServer.close();
108
117
  reactDTClients.forEach(ws => ws.close());
109
118
  reactDTClients.clear();
110
119
  }
120
+ // Close bridge servers and disconnect all clients
121
+ _bridgeServers.forEach(wss => {
122
+ wss.clients.forEach(ws => ws.close());
123
+ wss.close();
124
+ });
125
+ reduxClients.clear();
126
+ storageClients.clear();
127
+ networkClients.clear();
111
128
  });
112
129
 
113
130
  app.on('activate', () => {
@@ -336,6 +353,10 @@ function startReactDevToolsServer() {
336
353
  });
337
354
  });
338
355
 
356
+ ws.on('error', (err) => {
357
+ console.warn(`[ReactDT] Client error:`, err.message);
358
+ });
359
+
339
360
  ws.on('close', () => {
340
361
  reactDTClients.delete(ws);
341
362
  console.log(`[ReactDT] Client disconnected (total: ${reactDTClients.size})`);
@@ -380,6 +401,7 @@ function startBridgeServers() {
380
401
  function startBridge(port, name, clients, onEvent) {
381
402
  try {
382
403
  const wss = new WebSocketServer({ port });
404
+ _bridgeServers.push(wss);
383
405
  wss.on('error', (err) => {
384
406
  if (err.code === 'EADDRINUSE') {
385
407
  console.error(`[${name}] Port ${port} is already in use — another ReactoRadar or debugger may be running.`);
@@ -404,10 +426,19 @@ function startBridge(port, name, clients, onEvent) {
404
426
  }
405
427
  });
406
428
 
429
+ ws.on('error', (err) => {
430
+ console.warn(`[${name}] Client error:`, err.message);
431
+ });
432
+
407
433
  ws.on('close', () => {
408
434
  clients.delete(ws);
409
435
  if (clients.size === 0) {
410
436
  _send(`${name}-connected`, false);
437
+ // When every bridge has zero clients, tell the renderer to clear old data
438
+ if (reduxClients.size === 0 && storageClients.size === 0 && networkClients.size === 0) {
439
+ console.log('[Bridge] All device connections closed — sending clear signal');
440
+ _send('device-all-disconnected');
441
+ }
411
442
  }
412
443
  });
413
444
  });
@@ -589,6 +620,9 @@ function setupIPC() {
589
620
  });
590
621
 
591
622
  ipcMain.handle('fetch-changelog', async (_, version) => {
623
+ if (!version || typeof version !== 'string' || !/^[\d]+\.[\d]+\.[\d]+/.test(version)) {
624
+ return 'Invalid version.';
625
+ }
592
626
  return new Promise((resolve) => {
593
627
  https.get(`https://api.github.com/repos/sharanagouda/reactoradar/releases/tags/v${version}`, {
594
628
  headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
@@ -603,6 +637,44 @@ function setupIPC() {
603
637
  });
604
638
  });
605
639
 
640
+ // Fetch all releases for version history / rollback
641
+ ipcMain.handle('fetch-releases', async () => {
642
+ return new Promise((resolve) => {
643
+ https.get('https://api.github.com/repos/sharanagouda/reactoradar/releases?per_page=20', {
644
+ headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
645
+ }, (res) => {
646
+ let data = '';
647
+ res.on('data', d => data += d);
648
+ res.on('end', () => {
649
+ try {
650
+ const releases = JSON.parse(data);
651
+ if (!Array.isArray(releases)) { resolve([]); return; }
652
+ const mapped = [];
653
+ for (const r of releases) {
654
+ if (!r || typeof r !== 'object') continue;
655
+ const tag = r.tag_name || '';
656
+ const version = tag.replace(/^v/, '');
657
+ if (!version) continue; // skip entries with no version
658
+ const assets = Array.isArray(r.assets) ? r.assets : [];
659
+ mapped.push({
660
+ version,
661
+ tag,
662
+ name: r.name || tag || version,
663
+ date: r.published_at || null,
664
+ prerelease: !!r.prerelease,
665
+ body: r.body || '',
666
+ dmgUrl: (assets.find(a => a && a.name && a.name.endsWith('.dmg')) || {}).browser_download_url || '',
667
+ zipUrl: (assets.find(a => a && a.name && a.name.endsWith('.zip')) || {}).browser_download_url || '',
668
+ htmlUrl: r.html_url || '',
669
+ });
670
+ }
671
+ resolve(mapped);
672
+ } catch { resolve([]); }
673
+ });
674
+ }).on('error', () => resolve([]));
675
+ });
676
+ });
677
+
606
678
  ipcMain.on('install-update', () => {
607
679
  if (autoUpdater) {
608
680
  autoUpdater.quitAndInstall(false, true);
@@ -654,10 +726,9 @@ function setupIPC() {
654
726
  });
655
727
 
656
728
  ipcMain.on('start-native-logs', (_, platform) => {
657
- // Kill existing process group
729
+ // Kill existing process
658
730
  if (_nativeLogProcess) {
659
- try { process.kill(-_nativeLogProcess.pid); } catch {}
660
- try { _nativeLogProcess.kill(); } catch {}
731
+ try { _nativeLogProcess.kill('SIGTERM'); } catch {}
661
732
  _nativeLogProcess = null;
662
733
  }
663
734
 
@@ -665,9 +736,9 @@ function setupIPC() {
665
736
  let cmd, args;
666
737
 
667
738
  if (platform === 'android') {
668
- // adb logcat — filter for app-relevant logs
739
+ // adb logcat — show only new logs from now (not historical buffer)
669
740
  cmd = 'adb';
670
- args = ['logcat', '-v', 'threadtime', '*:W']; // Warnings and above
741
+ args = ['logcat', '-v', 'threadtime', '-T', '1', '*:W']; // -T 1 = last 1 line then real-time
671
742
  } else if (platform === 'ios-sim') {
672
743
  // xcrun simctl for iOS Simulator — use syslog style for parseable output
673
744
  cmd = 'xcrun';
@@ -682,9 +753,10 @@ function setupIPC() {
682
753
  }
683
754
 
684
755
  try {
685
- _nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
756
+ _nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
686
757
 
687
758
  _send('native-status', { connected: true, platform });
759
+ console.log(`[NativeLogs] Started ${cmd} ${args.join(' ')} (pid: ${_nativeLogProcess.pid})`);
688
760
 
689
761
  let buffer = '';
690
762
  _nativeLogProcess.stdout.on('data', (chunk) => {
@@ -703,6 +775,10 @@ function setupIPC() {
703
775
  if (text) _send('native-log', { level: 'error', message: text, source: 'stderr', ts: Date.now() });
704
776
  });
705
777
 
778
+ // Guard against stream errors (broken pipe, etc.)
779
+ _nativeLogProcess.stdout.on('error', () => {});
780
+ _nativeLogProcess.stderr.on('error', () => {});
781
+
706
782
  _nativeLogProcess.on('close', (code) => {
707
783
  _nativeLogProcess = null;
708
784
  _send('native-status', { connected: false, error: code ? `Process exited with code ${code}` : 'Disconnected' });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.4",
4
+ "version": "1.6.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/preload.js CHANGED
@@ -11,7 +11,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
11
11
  'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
12
12
  'console-event', 'perf-event', 'ga4-event', 'redux-connected', 'storage-connected', 'network-connected',
13
13
  'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available', 'update-downloaded', 'app-version', 'focus-search',
14
- 'native-log', 'native-status',
14
+ 'native-log', 'native-status', 'device-all-disconnected',
15
15
  ];
16
16
  if (allowed.includes(channel)) {
17
17
  ipcRenderer.removeAllListeners(channel);
@@ -36,4 +36,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
36
36
  stopNativeLogs: () => ipcRenderer.send('stop-native-logs'),
37
37
  detectNativePlatform: () => ipcRenderer.invoke('detect-native-platform'),
38
38
  fetchChangelog: (version) => ipcRenderer.invoke('fetch-changelog', version),
39
+ fetchReleases: () => ipcRenderer.invoke('fetch-releases'),
39
40
  });
package/styles.css CHANGED
@@ -1320,6 +1320,96 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1320
1320
  text-decoration: underline;
1321
1321
  }
1322
1322
 
1323
+ /* Version History */
1324
+ .version-history-list {
1325
+ max-height: 300px;
1326
+ overflow-y: auto;
1327
+ border: 1px solid var(--border);
1328
+ border-radius: 6px;
1329
+ background: var(--bg2);
1330
+ }
1331
+ .version-history-list::-webkit-scrollbar { width: 3px; }
1332
+ .version-history-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
1333
+ .version-row {
1334
+ display: flex;
1335
+ align-items: center;
1336
+ justify-content: space-between;
1337
+ padding: 8px 12px;
1338
+ border-bottom: 1px solid var(--border);
1339
+ font-size: 11px;
1340
+ transition: background 0.15s;
1341
+ }
1342
+ .version-row:last-child { border-bottom: none; }
1343
+ .version-row:hover { background: var(--bg3); }
1344
+ .version-row.version-current { background: color-mix(in srgb, var(--accent) 8%, transparent); }
1345
+ .version-info { display: flex; flex-direction: column; gap: 2px; }
1346
+ .version-tag { color: var(--text); font-weight: 600; font-size: 12px; }
1347
+ .version-date { color: var(--text-dim); font-size: 10px; }
1348
+ .version-badge {
1349
+ display: inline-block;
1350
+ background: var(--green);
1351
+ color: #000;
1352
+ font-size: 9px;
1353
+ font-weight: 700;
1354
+ padding: 1px 5px;
1355
+ border-radius: 3px;
1356
+ margin-left: 6px;
1357
+ vertical-align: middle;
1358
+ text-transform: uppercase;
1359
+ }
1360
+ .version-pre {
1361
+ display: inline-block;
1362
+ background: var(--yellow);
1363
+ color: #000;
1364
+ font-size: 9px;
1365
+ font-weight: 700;
1366
+ padding: 1px 5px;
1367
+ border-radius: 3px;
1368
+ margin-left: 4px;
1369
+ vertical-align: middle;
1370
+ }
1371
+ .version-actions { display: flex; gap: 6px; align-items: center; }
1372
+ .version-installed {
1373
+ font-size: 10px;
1374
+ color: var(--green);
1375
+ font-weight: 600;
1376
+ }
1377
+ .version-install-btn {
1378
+ background: var(--accent);
1379
+ color: #fff;
1380
+ border: none;
1381
+ border-radius: 4px;
1382
+ padding: 3px 10px;
1383
+ font-size: 10px;
1384
+ font-weight: 600;
1385
+ cursor: pointer;
1386
+ transition: opacity 0.15s;
1387
+ }
1388
+ .version-install-btn:hover { opacity: 0.85; }
1389
+ .version-notes-btn {
1390
+ background: transparent;
1391
+ color: var(--text-mid);
1392
+ border: 1px solid var(--border);
1393
+ border-radius: 4px;
1394
+ padding: 3px 8px;
1395
+ font-size: 10px;
1396
+ cursor: pointer;
1397
+ transition: color 0.15s, border-color 0.15s;
1398
+ }
1399
+ .version-npm-btn {
1400
+ background: transparent;
1401
+ color: var(--accent);
1402
+ border: 1px solid var(--accent);
1403
+ border-radius: 4px;
1404
+ padding: 3px 8px;
1405
+ font-size: 10px;
1406
+ font-family: var(--font-mono, monospace);
1407
+ cursor: pointer;
1408
+ transition: background 0.15s, color 0.15s;
1409
+ }
1410
+ .version-npm-btn:hover { background: var(--accent); color: #fff; }
1411
+ .version-notes-btn:hover { color: var(--accent); border-color: var(--accent); }
1412
+
1323
1413
  /* ─────────────────────────────────────────────────────────────────────────────
1324
1414
  SOURCES PANEL
1325
1415
  ───────────────────────────────────────────────────────────────────────────── */