reactoradar 1.6.3 → 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 +520 -45
- package/main.js +100 -9
- package/package.json +1 -1
- package/preload.js +3 -1
- package/styles.css +111 -0
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,15 +359,38 @@ 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() {
|
|
383
|
+
// If network detail is open, focus the detail search
|
|
384
|
+
if (state.activePanel === 'network' && state.network.selectedId) {
|
|
385
|
+
const wrap = $('detailSearchWrap');
|
|
386
|
+
const input = $('detailSearchInput');
|
|
387
|
+
if (wrap && input) {
|
|
388
|
+
wrap.style.display = 'flex';
|
|
389
|
+
input.focus();
|
|
390
|
+
input.select();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
306
394
|
const searchMap = {
|
|
307
395
|
console: 'consoleSearch',
|
|
308
396
|
network: 'netSearchInput',
|
|
@@ -330,8 +418,9 @@ if (window.electronAPI) {
|
|
|
330
418
|
}
|
|
331
419
|
});
|
|
332
420
|
|
|
333
|
-
window.electronAPI.on('app-version', (version) => {
|
|
421
|
+
window.electronAPI.on('app-version', (version, isPackaged) => {
|
|
334
422
|
state._appVersion = version;
|
|
423
|
+
state._isPackaged = !!isPackaged;
|
|
335
424
|
// Update anywhere the version is displayed
|
|
336
425
|
document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
|
|
337
426
|
});
|
|
@@ -366,6 +455,7 @@ function _applyUpdateBanner() {
|
|
|
366
455
|
if (!info) return;
|
|
367
456
|
const { current, latest, autoUpdate } = info;
|
|
368
457
|
const downloaded = state._updateDownloaded;
|
|
458
|
+
const targetVersion = downloaded || latest;
|
|
369
459
|
|
|
370
460
|
const el = $('aboutVersion');
|
|
371
461
|
if (el) {
|
|
@@ -376,39 +466,94 @@ function _applyUpdateBanner() {
|
|
|
376
466
|
}
|
|
377
467
|
}
|
|
378
468
|
|
|
379
|
-
// Remove old
|
|
469
|
+
// Remove old buttons if state changed
|
|
380
470
|
const oldBtn = $('updateBtn');
|
|
381
471
|
if (oldBtn && downloaded && !oldBtn.dataset.isRestart) oldBtn.parentElement?.remove();
|
|
472
|
+
const oldChangelog = $('changelogBtn');
|
|
473
|
+
if (oldChangelog && downloaded && !oldChangelog.dataset.updated) oldChangelog.remove();
|
|
474
|
+
|
|
475
|
+
const aboutEl = document.querySelector('.settings-about');
|
|
476
|
+
if (!aboutEl) return;
|
|
477
|
+
|
|
478
|
+
// Add "What's new?" link
|
|
479
|
+
if (!$('changelogBtn')) {
|
|
480
|
+
const link = document.createElement('div');
|
|
481
|
+
link.style.cssText = 'margin-top:6px;text-align:center';
|
|
482
|
+
link.innerHTML = `<span id="changelogBtn" class="about-link" style="font-size:10px;cursor:pointer" data-updated="${downloaded ? '1' : ''}">What's new in v${targetVersion}?</span>`;
|
|
483
|
+
aboutEl.appendChild(link);
|
|
484
|
+
$('changelogBtn')?.addEventListener('click', () => _showChangelog(targetVersion));
|
|
485
|
+
}
|
|
382
486
|
|
|
383
|
-
// Add update button
|
|
487
|
+
// Add update button
|
|
384
488
|
if (!$('updateBtn')) {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
btn.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
|
|
399
|
-
aboutEl.appendChild(btn);
|
|
400
|
-
} else {
|
|
401
|
-
// npx/manual — show download link
|
|
402
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
403
|
-
aboutEl.appendChild(btn);
|
|
404
|
-
$('updateBtn')?.addEventListener('click', () => {
|
|
405
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases');
|
|
406
|
-
});
|
|
407
|
-
}
|
|
489
|
+
const btn = document.createElement('div');
|
|
490
|
+
btn.style.cssText = 'margin-top:8px;text-align:center';
|
|
491
|
+
if (downloaded) {
|
|
492
|
+
btn.innerHTML = '<button id="updateBtn" data-is-restart="1" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Restart & Update to v' + downloaded + '</button>';
|
|
493
|
+
aboutEl.appendChild(btn);
|
|
494
|
+
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.installUpdate());
|
|
495
|
+
} else if (autoUpdate) {
|
|
496
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
|
|
497
|
+
aboutEl.appendChild(btn);
|
|
498
|
+
} else {
|
|
499
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
500
|
+
aboutEl.appendChild(btn);
|
|
501
|
+
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases'));
|
|
408
502
|
}
|
|
409
503
|
}
|
|
410
504
|
}
|
|
411
505
|
|
|
506
|
+
async function _showChangelog(version) {
|
|
507
|
+
if (!version || typeof version !== 'string') return;
|
|
508
|
+
|
|
509
|
+
// Remove existing modal
|
|
510
|
+
$('changelogModal')?.remove();
|
|
511
|
+
|
|
512
|
+
const safeVersion = esc(version);
|
|
513
|
+
const modal = document.createElement('div');
|
|
514
|
+
modal.id = 'changelogModal';
|
|
515
|
+
modal.className = 'changelog-modal-overlay';
|
|
516
|
+
modal.innerHTML = `
|
|
517
|
+
<div class="changelog-modal">
|
|
518
|
+
<div class="changelog-header">
|
|
519
|
+
<span class="changelog-title">What's New in v${safeVersion}</span>
|
|
520
|
+
<button class="changelog-close" id="changelogClose">×</button>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="changelog-body" id="changelogBody">
|
|
523
|
+
<div style="color:var(--text-dim);padding:20px;text-align:center">Loading release notes...</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>`;
|
|
526
|
+
document.body.appendChild(modal);
|
|
527
|
+
|
|
528
|
+
// Close handlers
|
|
529
|
+
$('changelogClose')?.addEventListener('click', () => modal.remove());
|
|
530
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
531
|
+
|
|
532
|
+
// Fetch changelog
|
|
533
|
+
try {
|
|
534
|
+
const notes = await window.electronAPI?.fetchChangelog(version);
|
|
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
|
+
}
|
|
541
|
+
if (body && notes) {
|
|
542
|
+
// Simple markdown-like rendering
|
|
543
|
+
body.innerHTML = notes
|
|
544
|
+
.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent);font-size:12px;font-weight:700;margin:12px 0 6px">$1</h3>')
|
|
545
|
+
.replace(/^## (.+)$/gm, '<h2 style="color:var(--text);font-size:14px;font-weight:700;margin:16px 0 8px">$1</h2>')
|
|
546
|
+
.replace(/^- \*\*(.+?)\*\*(.*)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6"><b style="color:var(--text)">$1</b><span style="color:var(--text-dim)">$2</span></div>')
|
|
547
|
+
.replace(/^- (.+)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6;color:var(--text-mid)">• $1</div>')
|
|
548
|
+
.replace(/`([^`]+)`/g, '<code style="background:var(--bg3);padding:1px 4px;border-radius:3px;color:var(--accent);font-size:10px">$1</code>')
|
|
549
|
+
.replace(/\n\n/g, '<br/>');
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
const body = $('changelogBody');
|
|
553
|
+
if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
412
557
|
function updateDeviceBanner(service, connected) {
|
|
413
558
|
state.connections[service] = connected;
|
|
414
559
|
const el = $('deviceStatus');
|
|
@@ -755,6 +900,41 @@ window.electronAPI?.on('console-event', addConsoleLog);
|
|
|
755
900
|
// ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
|
|
756
901
|
// Builds interactive, collapsible DOM nodes for objects/arrays.
|
|
757
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
|
+
|
|
758
938
|
function objPreview(val, maxLen) {
|
|
759
939
|
maxLen = maxLen || 80;
|
|
760
940
|
if (val === null) return 'null';
|
|
@@ -772,16 +952,16 @@ function objPreview(val, maxLen) {
|
|
|
772
952
|
return `(${val.length}) [${items.join(', ')}${suffix}]`;
|
|
773
953
|
}
|
|
774
954
|
if (typeof val === 'object') {
|
|
775
|
-
const
|
|
776
|
-
if (
|
|
955
|
+
const entries = collectEntries(val);
|
|
956
|
+
if (entries.length === 0) return '{}';
|
|
777
957
|
const items = [];
|
|
778
958
|
let len = 2;
|
|
779
|
-
for (let i = 0; i <
|
|
780
|
-
const s = `${
|
|
959
|
+
for (let i = 0; i < entries.length && len < maxLen; i++) {
|
|
960
|
+
const s = `${entries[i][0]}: ${primitivePreview(entries[i][1])}`;
|
|
781
961
|
len += s.length + 2;
|
|
782
962
|
items.push(s);
|
|
783
963
|
}
|
|
784
|
-
const suffix = items.length <
|
|
964
|
+
const suffix = items.length < entries.length ? ', ...' : '';
|
|
785
965
|
return `{${items.join(', ')}${suffix}}`;
|
|
786
966
|
}
|
|
787
967
|
return primitivePreview(val);
|
|
@@ -850,7 +1030,7 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
850
1030
|
function populateChildren() {
|
|
851
1031
|
if (populated) return;
|
|
852
1032
|
populated = true;
|
|
853
|
-
const entries =
|
|
1033
|
+
const entries = collectEntries(val);
|
|
854
1034
|
entries.forEach(([k, v]) => {
|
|
855
1035
|
children.appendChild(createTreeNode(k, v, true));
|
|
856
1036
|
});
|
|
@@ -1255,6 +1435,13 @@ function initNetworkPanel() {
|
|
|
1255
1435
|
<div class="net-detail-pane" id="netDetailPane">
|
|
1256
1436
|
<div class="net-detail-bar">
|
|
1257
1437
|
<div class="detail-tabs" id="netDetailTabs"></div>
|
|
1438
|
+
<div class="detail-search-wrap" id="detailSearchWrap" style="display:none">
|
|
1439
|
+
<input id="detailSearchInput" class="detail-search-input" placeholder="Search key or value..." />
|
|
1440
|
+
<span id="detailSearchCount" class="detail-search-count"></span>
|
|
1441
|
+
<button class="detail-search-nav" id="detailSearchPrev" title="Previous">▲</button>
|
|
1442
|
+
<button class="detail-search-nav" id="detailSearchNext" title="Next">▼</button>
|
|
1443
|
+
<button class="detail-search-close" id="detailSearchClose" title="Close search">×</button>
|
|
1444
|
+
</div>
|
|
1258
1445
|
<button class="detail-close" id="netDetailClose" title="Close">×</button>
|
|
1259
1446
|
</div>
|
|
1260
1447
|
<div class="detail-content" id="netDetailContent"></div>
|
|
@@ -1408,6 +1595,121 @@ function initNetworkPanel() {
|
|
|
1408
1595
|
// Close detail button
|
|
1409
1596
|
$('netDetailClose').addEventListener('click', closeNetDetail);
|
|
1410
1597
|
|
|
1598
|
+
// Detail panel search
|
|
1599
|
+
let _detailSearchMatches = [];
|
|
1600
|
+
let _detailSearchIdx = -1;
|
|
1601
|
+
|
|
1602
|
+
function _detailSearch() {
|
|
1603
|
+
const term = $('detailSearchInput')?.value?.trim().toLowerCase();
|
|
1604
|
+
const body = $('netDetailContent');
|
|
1605
|
+
if (!body || !term) { _detailClearSearch(); return; }
|
|
1606
|
+
|
|
1607
|
+
// Remove old highlights
|
|
1608
|
+
body.querySelectorAll('.detail-search-hl').forEach(el => {
|
|
1609
|
+
const parent = el.parentNode;
|
|
1610
|
+
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
1611
|
+
parent.normalize();
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
_detailSearchMatches = [];
|
|
1615
|
+
_detailSearchIdx = -1;
|
|
1616
|
+
|
|
1617
|
+
// Walk all text nodes and highlight matches
|
|
1618
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
|
|
1619
|
+
const textNodes = [];
|
|
1620
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
1621
|
+
|
|
1622
|
+
textNodes.forEach(node => {
|
|
1623
|
+
const text = node.textContent;
|
|
1624
|
+
const lower = text.toLowerCase();
|
|
1625
|
+
if (!lower.includes(term)) return;
|
|
1626
|
+
|
|
1627
|
+
const frag = document.createDocumentFragment();
|
|
1628
|
+
let lastIdx = 0;
|
|
1629
|
+
let idx;
|
|
1630
|
+
while ((idx = lower.indexOf(term, lastIdx)) !== -1) {
|
|
1631
|
+
if (idx > lastIdx) frag.appendChild(document.createTextNode(text.slice(lastIdx, idx)));
|
|
1632
|
+
const hl = document.createElement('span');
|
|
1633
|
+
hl.className = 'detail-search-hl';
|
|
1634
|
+
hl.textContent = text.slice(idx, idx + term.length);
|
|
1635
|
+
_detailSearchMatches.push(hl);
|
|
1636
|
+
frag.appendChild(hl);
|
|
1637
|
+
lastIdx = idx + term.length;
|
|
1638
|
+
}
|
|
1639
|
+
if (lastIdx < text.length) frag.appendChild(document.createTextNode(text.slice(lastIdx)));
|
|
1640
|
+
node.parentNode.replaceChild(frag, node);
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// Update count
|
|
1644
|
+
const countEl = $('detailSearchCount');
|
|
1645
|
+
if (countEl) countEl.textContent = _detailSearchMatches.length ? `${_detailSearchMatches.length} found` : 'No match';
|
|
1646
|
+
|
|
1647
|
+
// Navigate to first match
|
|
1648
|
+
if (_detailSearchMatches.length) _detailNavTo(0);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function _detailNavTo(idx) {
|
|
1652
|
+
// Remove active highlight from previous
|
|
1653
|
+
if (_detailSearchIdx >= 0 && _detailSearchMatches[_detailSearchIdx]) {
|
|
1654
|
+
_detailSearchMatches[_detailSearchIdx].classList.remove('active');
|
|
1655
|
+
}
|
|
1656
|
+
_detailSearchIdx = idx;
|
|
1657
|
+
const el = _detailSearchMatches[idx];
|
|
1658
|
+
if (!el) return;
|
|
1659
|
+
el.classList.add('active');
|
|
1660
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1661
|
+
// Update count
|
|
1662
|
+
const countEl = $('detailSearchCount');
|
|
1663
|
+
if (countEl) countEl.textContent = `${idx + 1}/${_detailSearchMatches.length}`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function _detailClearSearch() {
|
|
1667
|
+
const body = $('netDetailContent');
|
|
1668
|
+
if (body) {
|
|
1669
|
+
body.querySelectorAll('.detail-search-hl').forEach(el => {
|
|
1670
|
+
const parent = el.parentNode;
|
|
1671
|
+
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
1672
|
+
parent.normalize();
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
_detailSearchMatches = [];
|
|
1676
|
+
_detailSearchIdx = -1;
|
|
1677
|
+
const countEl = $('detailSearchCount');
|
|
1678
|
+
if (countEl) countEl.textContent = '';
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
$('detailSearchInput')?.addEventListener('input', () => {
|
|
1682
|
+
clearTimeout($('detailSearchInput')._debounce);
|
|
1683
|
+
$('detailSearchInput')._debounce = setTimeout(_detailSearch, 200);
|
|
1684
|
+
});
|
|
1685
|
+
$('detailSearchInput')?.addEventListener('keydown', (e) => {
|
|
1686
|
+
if (e.key === 'Enter') {
|
|
1687
|
+
e.preventDefault();
|
|
1688
|
+
if (!_detailSearchMatches.length) return;
|
|
1689
|
+
const next = e.shiftKey
|
|
1690
|
+
? (_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length
|
|
1691
|
+
: (_detailSearchIdx + 1) % _detailSearchMatches.length;
|
|
1692
|
+
_detailNavTo(next);
|
|
1693
|
+
}
|
|
1694
|
+
if (e.key === 'Escape') {
|
|
1695
|
+
_detailClearSearch();
|
|
1696
|
+
$('detailSearchWrap').style.display = 'none';
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
$('detailSearchNext')?.addEventListener('click', () => {
|
|
1700
|
+
if (!_detailSearchMatches.length) return;
|
|
1701
|
+
_detailNavTo((_detailSearchIdx + 1) % _detailSearchMatches.length);
|
|
1702
|
+
});
|
|
1703
|
+
$('detailSearchPrev')?.addEventListener('click', () => {
|
|
1704
|
+
if (!_detailSearchMatches.length) return;
|
|
1705
|
+
_detailNavTo((_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length);
|
|
1706
|
+
});
|
|
1707
|
+
$('detailSearchClose')?.addEventListener('click', () => {
|
|
1708
|
+
_detailClearSearch();
|
|
1709
|
+
$('detailSearchInput').value = '';
|
|
1710
|
+
$('detailSearchWrap').style.display = 'none';
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1411
1713
|
buildNetHeader();
|
|
1412
1714
|
}
|
|
1413
1715
|
|
|
@@ -1828,14 +2130,38 @@ function closeNetDetail() {
|
|
|
1828
2130
|
);
|
|
1829
2131
|
}
|
|
1830
2132
|
|
|
2133
|
+
function _estimateSize(val) {
|
|
2134
|
+
if (val == null) return 0;
|
|
2135
|
+
if (typeof val === 'string') return val.length;
|
|
2136
|
+
try { return JSON.stringify(val).length; } catch { return 0; }
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function _formatBytes(bytes) {
|
|
2140
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
2141
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2142
|
+
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1831
2145
|
function renderNetDetailTabs(r) {
|
|
1832
2146
|
const tabs = $('netDetailTabs');
|
|
1833
2147
|
tabs.innerHTML = '';
|
|
1834
|
-
|
|
1835
|
-
|
|
2148
|
+
|
|
2149
|
+
const tabDefs = [
|
|
2150
|
+
{ label: 'Headers', key: 'headers' },
|
|
2151
|
+
{ label: 'Request', key: 'request', sizeFrom: 'requestBody' },
|
|
2152
|
+
{ label: 'Preview', key: 'preview', sizeFrom: 'responseBody' },
|
|
2153
|
+
{ label: 'Response', key: 'response', sizeFrom: 'responseBody' },
|
|
2154
|
+
];
|
|
2155
|
+
|
|
2156
|
+
tabDefs.forEach(({ label, key, sizeFrom }) => {
|
|
1836
2157
|
const btn = document.createElement('button');
|
|
1837
2158
|
btn.className = 'detail-tab' + (r._tab === key ? ' active' : '');
|
|
1838
|
-
|
|
2159
|
+
let text = label;
|
|
2160
|
+
if (sizeFrom && r[sizeFrom]) {
|
|
2161
|
+
const size = _estimateSize(r[sizeFrom]);
|
|
2162
|
+
if (size > 0) text += ` (${_formatBytes(size)})`;
|
|
2163
|
+
}
|
|
2164
|
+
btn.textContent = text;
|
|
1839
2165
|
btn.addEventListener('click', () => {
|
|
1840
2166
|
r._tab = key;
|
|
1841
2167
|
tabs.querySelectorAll('.detail-tab').forEach(b => b.classList.remove('active'));
|
|
@@ -1844,6 +2170,12 @@ function renderNetDetailTabs(r) {
|
|
|
1844
2170
|
});
|
|
1845
2171
|
tabs.appendChild(btn);
|
|
1846
2172
|
});
|
|
2173
|
+
|
|
2174
|
+
// Show search box for Preview/Response tabs
|
|
2175
|
+
const searchWrap = $('detailSearchWrap');
|
|
2176
|
+
if (searchWrap) {
|
|
2177
|
+
searchWrap.style.display = (r._tab === 'preview' || r._tab === 'response' || r._tab === 'headers') ? 'flex' : 'none';
|
|
2178
|
+
}
|
|
1847
2179
|
}
|
|
1848
2180
|
|
|
1849
2181
|
function renderNetDetailContent(r) {
|
|
@@ -3036,10 +3368,18 @@ function initNativeLogsPanel() {
|
|
|
3036
3368
|
<div class="native-log-list" id="nativeLogList"></div>
|
|
3037
3369
|
</div>`;
|
|
3038
3370
|
|
|
3039
|
-
// Connect buttons
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
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'); });
|
|
3043
3383
|
$('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
|
|
3044
3384
|
|
|
3045
3385
|
// Clear buttons (toolbar + logs area)
|
|
@@ -3649,6 +3989,27 @@ function initSettingsPanel() {
|
|
|
3649
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
|
|
3650
3990
|
</div>
|
|
3651
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@<version></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>
|
|
3652
4013
|
</div>
|
|
3653
4014
|
</div>
|
|
3654
4015
|
</div>`;
|
|
@@ -3755,6 +4116,120 @@ function initSettingsPanel() {
|
|
|
3755
4116
|
|
|
3756
4117
|
// Apply update banner if update info arrived before settings panel was created
|
|
3757
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
|
+
});
|
|
3758
4233
|
}
|
|
3759
4234
|
|
|
3760
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
|
});
|
|
@@ -588,6 +619,62 @@ function setupIPC() {
|
|
|
588
619
|
}
|
|
589
620
|
});
|
|
590
621
|
|
|
622
|
+
ipcMain.handle('fetch-changelog', async (_, version) => {
|
|
623
|
+
if (!version || typeof version !== 'string' || !/^[\d]+\.[\d]+\.[\d]+/.test(version)) {
|
|
624
|
+
return 'Invalid version.';
|
|
625
|
+
}
|
|
626
|
+
return new Promise((resolve) => {
|
|
627
|
+
https.get(`https://api.github.com/repos/sharanagouda/reactoradar/releases/tags/v${version}`, {
|
|
628
|
+
headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
|
|
629
|
+
}, (res) => {
|
|
630
|
+
let data = '';
|
|
631
|
+
res.on('data', d => data += d);
|
|
632
|
+
res.on('end', () => {
|
|
633
|
+
try { resolve(JSON.parse(data).body || 'No release notes available.'); }
|
|
634
|
+
catch { resolve('Could not fetch release notes.'); }
|
|
635
|
+
});
|
|
636
|
+
}).on('error', () => resolve('Could not connect to GitHub.'));
|
|
637
|
+
});
|
|
638
|
+
});
|
|
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
|
+
|
|
591
678
|
ipcMain.on('install-update', () => {
|
|
592
679
|
if (autoUpdater) {
|
|
593
680
|
autoUpdater.quitAndInstall(false, true);
|
|
@@ -639,10 +726,9 @@ function setupIPC() {
|
|
|
639
726
|
});
|
|
640
727
|
|
|
641
728
|
ipcMain.on('start-native-logs', (_, platform) => {
|
|
642
|
-
// Kill existing process
|
|
729
|
+
// Kill existing process
|
|
643
730
|
if (_nativeLogProcess) {
|
|
644
|
-
try {
|
|
645
|
-
try { _nativeLogProcess.kill(); } catch {}
|
|
731
|
+
try { _nativeLogProcess.kill('SIGTERM'); } catch {}
|
|
646
732
|
_nativeLogProcess = null;
|
|
647
733
|
}
|
|
648
734
|
|
|
@@ -650,9 +736,9 @@ function setupIPC() {
|
|
|
650
736
|
let cmd, args;
|
|
651
737
|
|
|
652
738
|
if (platform === 'android') {
|
|
653
|
-
// adb logcat —
|
|
739
|
+
// adb logcat — show only new logs from now (not historical buffer)
|
|
654
740
|
cmd = 'adb';
|
|
655
|
-
args = ['logcat', '-v', 'threadtime', '*:W']; //
|
|
741
|
+
args = ['logcat', '-v', 'threadtime', '-T', '1', '*:W']; // -T 1 = last 1 line then real-time
|
|
656
742
|
} else if (platform === 'ios-sim') {
|
|
657
743
|
// xcrun simctl for iOS Simulator — use syslog style for parseable output
|
|
658
744
|
cmd = 'xcrun';
|
|
@@ -667,9 +753,10 @@ function setupIPC() {
|
|
|
667
753
|
}
|
|
668
754
|
|
|
669
755
|
try {
|
|
670
|
-
_nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe']
|
|
756
|
+
_nativeLogProcess = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
671
757
|
|
|
672
758
|
_send('native-status', { connected: true, platform });
|
|
759
|
+
console.log(`[NativeLogs] Started ${cmd} ${args.join(' ')} (pid: ${_nativeLogProcess.pid})`);
|
|
673
760
|
|
|
674
761
|
let buffer = '';
|
|
675
762
|
_nativeLogProcess.stdout.on('data', (chunk) => {
|
|
@@ -688,6 +775,10 @@ function setupIPC() {
|
|
|
688
775
|
if (text) _send('native-log', { level: 'error', message: text, source: 'stderr', ts: Date.now() });
|
|
689
776
|
});
|
|
690
777
|
|
|
778
|
+
// Guard against stream errors (broken pipe, etc.)
|
|
779
|
+
_nativeLogProcess.stdout.on('error', () => {});
|
|
780
|
+
_nativeLogProcess.stderr.on('error', () => {});
|
|
781
|
+
|
|
691
782
|
_nativeLogProcess.on('close', (code) => {
|
|
692
783
|
_nativeLogProcess = null;
|
|
693
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
|
+
"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);
|
|
@@ -35,4 +35,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
35
35
|
startNativeLogs: (platform) => ipcRenderer.send('start-native-logs', platform),
|
|
36
36
|
stopNativeLogs: () => ipcRenderer.send('stop-native-logs'),
|
|
37
37
|
detectNativePlatform: () => ipcRenderer.invoke('detect-native-platform'),
|
|
38
|
+
fetchChangelog: (version) => ipcRenderer.invoke('fetch-changelog', version),
|
|
39
|
+
fetchReleases: () => ipcRenderer.invoke('fetch-releases'),
|
|
38
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
|
───────────────────────────────────────────────────────────────────────────── */
|
|
@@ -1682,6 +1772,27 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
|
|
|
1682
1772
|
.native-fatal { background: rgba(255,94,114,.08); }
|
|
1683
1773
|
.native-fatal .native-log-msg { color: var(--red); font-weight: 700; }
|
|
1684
1774
|
|
|
1775
|
+
/* ── Detail Panel Search ───────────────────────────────────────────────────── */
|
|
1776
|
+
.detail-search-wrap { display: flex; align-items: center; gap: 4px; margin-left: auto; padding: 0 6px; }
|
|
1777
|
+
.detail-search-input { width: 150px; font-size: 10px; padding: 3px 6px; border: 1px solid var(--border); background: var(--bg1); color: var(--text); border-radius: 3px; outline: none; }
|
|
1778
|
+
.detail-search-input:focus { border-color: var(--accent); }
|
|
1779
|
+
.detail-search-count { font-size: 9px; color: var(--text-dim); white-space: nowrap; min-width: 45px; }
|
|
1780
|
+
.detail-search-nav { background: transparent; border: 1px solid var(--border); color: var(--text-dim); font-size: 9px; width: 18px; height: 18px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
1781
|
+
.detail-search-nav:hover { background: var(--bg3); color: var(--text); }
|
|
1782
|
+
.detail-search-close { background: transparent; border: none; color: var(--text-dim); font-size: 12px; cursor: pointer; padding: 0 2px; }
|
|
1783
|
+
.detail-search-close:hover { color: var(--text); }
|
|
1784
|
+
.detail-search-hl { background: rgba(255,213,79,.3); border-radius: 2px; padding: 0 1px; }
|
|
1785
|
+
.detail-search-hl.active { background: rgba(255,213,79,.7); outline: 1px solid rgba(255,213,79,.9); }
|
|
1786
|
+
|
|
1787
|
+
/* ── Changelog Modal ───────────────────────────────────────────────────────── */
|
|
1788
|
+
.changelog-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 9999; display: flex; align-items: center; justify-content: center; }
|
|
1789
|
+
.changelog-modal { background: var(--bg1); border: 1px solid var(--border); border-radius: 10px; width: 520px; max-width: 90vw; max-height: 70vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,.4); }
|
|
1790
|
+
.changelog-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
1791
|
+
.changelog-title { font-size: 13px; font-weight: 700; color: var(--text); }
|
|
1792
|
+
.changelog-close { background: transparent; border: none; color: var(--text-dim); font-size: 18px; cursor: pointer; padding: 0 4px; }
|
|
1793
|
+
.changelog-close:hover { color: var(--text); }
|
|
1794
|
+
.changelog-body { flex: 1; overflow-y: auto; padding: 16px; font-size: 11px; line-height: 1.6; color: var(--text-mid); }
|
|
1795
|
+
|
|
1685
1796
|
/* ── Support Button ────────────────────────────────────────────────────────── */
|
|
1686
1797
|
.support-btn { background: linear-gradient(135deg, #ff813f, #ff5e72); color: #fff; border: none; padding: 8px 20px; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.15s; letter-spacing: 0.3px; }
|
|
1687
1798
|
.support-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,94,114,.3); }
|