reactoradar 1.6.6 → 1.6.8
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/AGENTS.md +341 -0
- package/app.js +18 -19
- package/init.js +199 -0
- package/package.json +4 -2
- package/panels/console.js +789 -0
- package/panels/ga4.js +331 -0
- package/panels/native.js +260 -0
- package/panels/network.js +972 -0
- package/panels/performance.js +188 -0
- package/panels/react.js +23 -0
- package/panels/redux.js +441 -0
- package/panels/settings.js +791 -0
- package/panels/sources.js +289 -0
- package/panels/storage.js +191 -0
- package/sdk/RNDebugSDK.js +100 -38
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
// ─── Console Panel + Shared Renderers ─────────────────────────────────────
|
|
2
|
+
// Load saved log level filters from localStorage
|
|
3
|
+
function getStoredLogLevels() {
|
|
4
|
+
try {
|
|
5
|
+
const saved = localStorage.getItem('rn-debug-log-levels');
|
|
6
|
+
if (saved) return JSON.parse(saved);
|
|
7
|
+
} catch {}
|
|
8
|
+
return { log: true, info: true, warn: true, error: true, debug: true, redux: false };
|
|
9
|
+
}
|
|
10
|
+
function setStoredLogLevels(levels) {
|
|
11
|
+
try { localStorage.setItem('rn-debug-log-levels', JSON.stringify(levels)); } catch {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function initConsolePanel() {
|
|
15
|
+
const panel = $('panel-console');
|
|
16
|
+
if (!panel) return;
|
|
17
|
+
const levels = getStoredLogLevels();
|
|
18
|
+
state.console.levelFilters = levels;
|
|
19
|
+
state.console.showRedux = !!levels.redux;
|
|
20
|
+
|
|
21
|
+
panel.innerHTML = `
|
|
22
|
+
<div class="panel-toolbar">
|
|
23
|
+
<span class="panel-label">Console</span>
|
|
24
|
+
<span class="badge" id="cBadge">0</span>
|
|
25
|
+
<input id="consoleSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter logs..." />
|
|
26
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
27
|
+
<button class="panel-clear-btn" id="consoleExport" title="Export logs as JSON">Export</button>
|
|
28
|
+
<button class="panel-clear-btn" id="consoleClear" title="Clear console">Clear</button>
|
|
29
|
+
<div class="console-level-dropdown" id="consoleLevelDropdown">
|
|
30
|
+
<button class="console-level-btn" id="consoleLevelBtn">Levels ▾</button>
|
|
31
|
+
<div class="console-level-menu" id="consoleLevelMenu">
|
|
32
|
+
<label class="console-level-option"><input type="checkbox" data-level="log" ${levels.log ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--text-mid)"></span>Log</label>
|
|
33
|
+
<label class="console-level-option"><input type="checkbox" data-level="info" ${levels.info ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent)"></span>Info</label>
|
|
34
|
+
<label class="console-level-option"><input type="checkbox" data-level="warn" ${levels.warn ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--yellow)"></span>Warn</label>
|
|
35
|
+
<label class="console-level-option"><input type="checkbox" data-level="error" ${levels.error ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--red)"></span>Error</label>
|
|
36
|
+
<label class="console-level-option"><input type="checkbox" data-level="debug" ${levels.debug ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent2)"></span>Debug</label>
|
|
37
|
+
<div style="border-top:1px solid var(--border);margin:4px 0"></div>
|
|
38
|
+
<label class="console-level-option"><input type="checkbox" data-level="redux" ${levels.redux ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--green)"></span>Redux Actions</label>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="scroll-area" id="consoleList">
|
|
44
|
+
<div class="empty-state" id="consoleEmpty">
|
|
45
|
+
<div class="icon">⬛</div>
|
|
46
|
+
<div class="label">No logs yet</div>
|
|
47
|
+
<div class="hint">Logs will appear here automatically</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="console-find-bar" id="consoleFindBar" style="display:none">
|
|
51
|
+
<input id="consoleFindInput" class="console-find-input" placeholder="Find in logs... (Cmd+F)" />
|
|
52
|
+
<span id="consoleFindCount" class="console-find-count"></span>
|
|
53
|
+
<button class="console-find-btn" id="consoleFindPrev" title="Previous">▲</button>
|
|
54
|
+
<button class="console-find-btn" id="consoleFindNext" title="Next">▼</button>
|
|
55
|
+
<button class="console-find-btn" id="consoleFindClose" title="Close (Esc)">✕</button>
|
|
56
|
+
</div>`;
|
|
57
|
+
|
|
58
|
+
// Search filter
|
|
59
|
+
$('consoleSearch').addEventListener('input', (e) => {
|
|
60
|
+
state.console.searchFilter = e.target.value.toLowerCase().trim();
|
|
61
|
+
renderConsole();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Level dropdown toggle
|
|
65
|
+
$('consoleLevelBtn').addEventListener('click', (e) => {
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
$('consoleLevelMenu').classList.toggle('open');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Close dropdown when clicking outside
|
|
71
|
+
document.addEventListener('click', (e) => {
|
|
72
|
+
if (!e.target.closest('#consoleLevelDropdown')) {
|
|
73
|
+
$('consoleLevelMenu')?.classList.remove('open');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Level checkbox changes
|
|
78
|
+
$('consoleLevelMenu').addEventListener('change', (e) => {
|
|
79
|
+
const checkbox = e.target;
|
|
80
|
+
const level = checkbox.dataset.level;
|
|
81
|
+
if (level) {
|
|
82
|
+
state.console.levelFilters[level] = checkbox.checked;
|
|
83
|
+
if (level === 'redux') state.console.showRedux = checkbox.checked;
|
|
84
|
+
setStoredLogLevels(state.console.levelFilters);
|
|
85
|
+
updateLevelBtnText();
|
|
86
|
+
renderConsole();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
updateLevelBtnText();
|
|
91
|
+
|
|
92
|
+
$('consoleExport')?.addEventListener('click', () => {
|
|
93
|
+
const data = JSON.stringify(state.console.logs, null, 2);
|
|
94
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
95
|
+
const url = URL.createObjectURL(blob);
|
|
96
|
+
const a = document.createElement('a');
|
|
97
|
+
a.href = url; a.download = `reactoradar-console-${Date.now()}.json`; a.click();
|
|
98
|
+
URL.revokeObjectURL(url);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
$('consoleClear').addEventListener('click', () => {
|
|
102
|
+
state.console.logs = [];
|
|
103
|
+
_consolePending = [];
|
|
104
|
+
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
105
|
+
$('cBadge').textContent = '0';
|
|
106
|
+
renderConsole();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Find bar (Cmd+F)
|
|
110
|
+
let _findMatches = [];
|
|
111
|
+
let _findIdx = -1;
|
|
112
|
+
|
|
113
|
+
function doFind(term) {
|
|
114
|
+
// Clear previous highlights
|
|
115
|
+
document.querySelectorAll('.console-find-highlight').forEach(el => {
|
|
116
|
+
el.replaceWith(el.textContent);
|
|
117
|
+
});
|
|
118
|
+
_findMatches = [];
|
|
119
|
+
_findIdx = -1;
|
|
120
|
+
if (!term) { $('consoleFindCount').textContent = ''; return; }
|
|
121
|
+
|
|
122
|
+
const rows = document.querySelectorAll('#consoleList .log-row');
|
|
123
|
+
rows.forEach(row => {
|
|
124
|
+
const text = row.textContent.toLowerCase();
|
|
125
|
+
if (text.includes(term.toLowerCase())) _findMatches.push(row);
|
|
126
|
+
});
|
|
127
|
+
$('consoleFindCount').textContent = _findMatches.length ? `${_findMatches.length} found` : 'No matches';
|
|
128
|
+
if (_findMatches.length) { _findIdx = 0; _findMatches[0].scrollIntoView({ block: 'nearest' }); _findMatches[0].style.outline = '1px solid var(--accent)'; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findNav(dir) {
|
|
132
|
+
if (!_findMatches.length) return;
|
|
133
|
+
if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
|
|
134
|
+
_findIdx = (_findIdx + dir + _findMatches.length) % _findMatches.length;
|
|
135
|
+
_findMatches[_findIdx].scrollIntoView({ block: 'nearest' });
|
|
136
|
+
_findMatches[_findIdx].style.outline = '1px solid var(--accent)';
|
|
137
|
+
$('consoleFindCount').textContent = `${_findIdx + 1}/${_findMatches.length}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
$('consoleFindInput').addEventListener('input', (e) => doFind(e.target.value));
|
|
141
|
+
$('consoleFindPrev').addEventListener('click', () => findNav(-1));
|
|
142
|
+
$('consoleFindNext').addEventListener('click', () => findNav(1));
|
|
143
|
+
$('consoleFindClose').addEventListener('click', () => {
|
|
144
|
+
$('consoleFindBar').style.display = 'none';
|
|
145
|
+
if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
|
|
146
|
+
_findMatches = []; _findIdx = -1;
|
|
147
|
+
$('consoleFindInput').value = '';
|
|
148
|
+
$('consoleFindCount').textContent = '';
|
|
149
|
+
});
|
|
150
|
+
$('consoleFindInput').addEventListener('keydown', (e) => {
|
|
151
|
+
if (e.key === 'Escape') $('consoleFindClose').click();
|
|
152
|
+
if (e.key === 'Enter') findNav(e.shiftKey ? -1 : 1);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function updateLevelBtnText() {
|
|
157
|
+
const levels = state.console.levelFilters;
|
|
158
|
+
const logLevels = { log: levels.log, info: levels.info, warn: levels.warn, error: levels.error, debug: levels.debug };
|
|
159
|
+
const allOn = Object.values(logLevels).every(v => v);
|
|
160
|
+
const allOff = Object.values(logLevels).every(v => !v);
|
|
161
|
+
const btn = $('consoleLevelBtn');
|
|
162
|
+
if (!btn) return;
|
|
163
|
+
let text = '';
|
|
164
|
+
if (allOn) text = 'All Levels';
|
|
165
|
+
else if (allOff) text = 'None';
|
|
166
|
+
else text = Object.entries(logLevels).filter(([, v]) => v).map(([k]) => k.charAt(0).toUpperCase() + k.slice(1)).join(', ');
|
|
167
|
+
if (levels.redux) text += (text ? ' + ' : '') + 'Redux';
|
|
168
|
+
btn.textContent = text + ' ▾';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Console is fed via IPC (network-event handled in IPC section above)
|
|
172
|
+
|
|
173
|
+
// ─── Toast Notifications ─────────────────────────────────────────────────────
|
|
174
|
+
let _toastContainer = null;
|
|
175
|
+
const _activeToasts = {};
|
|
176
|
+
|
|
177
|
+
function getToastsEnabled() {
|
|
178
|
+
try { return localStorage.getItem('rn-debug-toasts') !== 'false'; } catch { return true; }
|
|
179
|
+
}
|
|
180
|
+
function setToastsEnabled(v) {
|
|
181
|
+
try { localStorage.setItem('rn-debug-toasts', v ? 'true' : 'false'); } catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function showToast(message, type, targetPanel) {
|
|
185
|
+
if (!getToastsEnabled()) return;
|
|
186
|
+
if (!_toastContainer) {
|
|
187
|
+
_toastContainer = document.createElement('div');
|
|
188
|
+
_toastContainer.id = 'toastContainer';
|
|
189
|
+
_toastContainer.className = 'toast-container';
|
|
190
|
+
document.body.appendChild(_toastContainer);
|
|
191
|
+
}
|
|
192
|
+
// Don't show toast if user is already on the target panel
|
|
193
|
+
if (targetPanel && state.activePanel === targetPanel) return;
|
|
194
|
+
|
|
195
|
+
// Deduplicate: if same message already showing, increment count
|
|
196
|
+
const key = `${type}:${message}`;
|
|
197
|
+
if (_activeToasts[key] && _activeToasts[key].el.parentNode) {
|
|
198
|
+
const existing = _activeToasts[key];
|
|
199
|
+
existing.count++;
|
|
200
|
+
const msgEl = existing.el.querySelector('.toast-msg');
|
|
201
|
+
if (msgEl) msgEl.textContent = `${message} (${existing.count})`;
|
|
202
|
+
// Reset auto-remove timer
|
|
203
|
+
clearTimeout(existing.timer);
|
|
204
|
+
existing.timer = setTimeout(() => {
|
|
205
|
+
if (existing.el.parentNode) existing.el.remove();
|
|
206
|
+
delete _activeToasts[key];
|
|
207
|
+
}, 5000);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const toast = document.createElement('div');
|
|
212
|
+
toast.className = `toast toast-${type || 'info'}`;
|
|
213
|
+
toast.innerHTML = `<span class="toast-msg">${esc(message)}</span>`;
|
|
214
|
+
if (targetPanel) {
|
|
215
|
+
const btn = document.createElement('span');
|
|
216
|
+
btn.className = 'toast-action';
|
|
217
|
+
btn.textContent = 'View';
|
|
218
|
+
btn.addEventListener('click', () => { switchPanel(targetPanel); toast.remove(); delete _activeToasts[key]; });
|
|
219
|
+
toast.appendChild(btn);
|
|
220
|
+
}
|
|
221
|
+
const close = document.createElement('span');
|
|
222
|
+
close.className = 'toast-close';
|
|
223
|
+
close.textContent = '✕';
|
|
224
|
+
close.addEventListener('click', () => { toast.remove(); delete _activeToasts[key]; });
|
|
225
|
+
toast.appendChild(close);
|
|
226
|
+
|
|
227
|
+
_toastContainer.appendChild(toast);
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
if (toast.parentNode) toast.remove();
|
|
230
|
+
delete _activeToasts[key];
|
|
231
|
+
}, 5000);
|
|
232
|
+
_activeToasts[key] = { el: toast, count: 1, timer };
|
|
233
|
+
// Keep max 3 toasts
|
|
234
|
+
const toasts = _toastContainer.querySelectorAll('.toast');
|
|
235
|
+
if (toasts.length > 3) { toasts[0].remove(); }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Batched console append (fixes re-render performance) ────────────────────
|
|
239
|
+
let _consolePending = [];
|
|
240
|
+
let _consoleRAF = null;
|
|
241
|
+
|
|
242
|
+
let _lastLogMsg = '';
|
|
243
|
+
let _lastLogRow = null;
|
|
244
|
+
let _lastLogCount = 1;
|
|
245
|
+
|
|
246
|
+
const MAX_CONSOLE_LOGS = 5000;
|
|
247
|
+
|
|
248
|
+
function addConsoleLog(event) {
|
|
249
|
+
state.console.logs.push(event);
|
|
250
|
+
// Cap in-memory logs to prevent memory leak
|
|
251
|
+
if (state.console.logs.length > MAX_CONSOLE_LOGS) {
|
|
252
|
+
state.console.logs = state.console.logs.slice(-MAX_CONSOLE_LOGS);
|
|
253
|
+
}
|
|
254
|
+
_consolePending.push(event);
|
|
255
|
+
|
|
256
|
+
// Batch DOM updates via rAF — only one paint per frame
|
|
257
|
+
if (!_consoleRAF) {
|
|
258
|
+
_consoleRAF = requestAnimationFrame(flushConsoleBatch);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function flushConsoleBatch() {
|
|
263
|
+
_consoleRAF = null;
|
|
264
|
+
const batch = _consolePending;
|
|
265
|
+
_consolePending = [];
|
|
266
|
+
if (!batch.length) return;
|
|
267
|
+
|
|
268
|
+
$('cBadge').textContent = state.console.logs.length;
|
|
269
|
+
|
|
270
|
+
const list = $('consoleList');
|
|
271
|
+
const empty = $('consoleEmpty');
|
|
272
|
+
if (!list) return;
|
|
273
|
+
|
|
274
|
+
const { levelFilters, searchFilter } = state.console;
|
|
275
|
+
const frag = document.createDocumentFragment();
|
|
276
|
+
let added = 0;
|
|
277
|
+
|
|
278
|
+
batch.forEach(l => {
|
|
279
|
+
// Redux logs use showRedux flag; regular logs use levelFilters
|
|
280
|
+
if (l.level === 'redux') {
|
|
281
|
+
if (!state.console.showRedux) return;
|
|
282
|
+
} else if (levelFilters && !levelFilters[l.level]) return;
|
|
283
|
+
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return;
|
|
284
|
+
|
|
285
|
+
// Group consecutive identical messages
|
|
286
|
+
const msgKey = `${l.level}:${l.message || ''}`;
|
|
287
|
+
if (msgKey === _lastLogMsg && _lastLogRow && _lastLogRow.parentNode) {
|
|
288
|
+
_lastLogCount++;
|
|
289
|
+
let badge = _lastLogRow.querySelector('.log-group-badge');
|
|
290
|
+
if (!badge) {
|
|
291
|
+
badge = document.createElement('span');
|
|
292
|
+
badge.className = 'log-group-badge';
|
|
293
|
+
_lastLogRow.insertBefore(badge, _lastLogRow.firstChild);
|
|
294
|
+
}
|
|
295
|
+
badge.textContent = _lastLogCount;
|
|
296
|
+
return; // Don't add a new row
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_lastLogMsg = msgKey;
|
|
300
|
+
_lastLogCount = 1;
|
|
301
|
+
const row = buildLogRow(l);
|
|
302
|
+
_lastLogRow = row;
|
|
303
|
+
frag.appendChild(row);
|
|
304
|
+
added++;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (added > 0) {
|
|
308
|
+
// Hide empty state as soon as we have visible rows
|
|
309
|
+
if (empty) empty.style.display = 'none';
|
|
310
|
+
// Auto-scroll only if user is already near the bottom (within 150px)
|
|
311
|
+
const wasAtBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
312
|
+
list.appendChild(frag);
|
|
313
|
+
// Keep DOM size manageable — remove oldest rows
|
|
314
|
+
const rows = list.querySelectorAll('.log-row');
|
|
315
|
+
const MAX_DOM_ROWS = 2000;
|
|
316
|
+
if (rows.length > MAX_DOM_ROWS) {
|
|
317
|
+
const toRemove = rows.length - MAX_DOM_ROWS;
|
|
318
|
+
for (let i = 0; i < toRemove; i++) rows[i].remove();
|
|
319
|
+
}
|
|
320
|
+
if (wasAtBottom) list.scrollTop = list.scrollHeight;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
// NOTE: console-event IPC listener is registered in init.js
|
|
326
|
+
|
|
327
|
+
// ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
|
|
328
|
+
// Builds interactive, collapsible DOM nodes for objects/arrays.
|
|
329
|
+
|
|
330
|
+
// Collect all entries for an object: own data properties + prototype getter values.
|
|
331
|
+
// Getter-derived keys use the clean name (e.g. "deliveryId") and skip backing
|
|
332
|
+
// fields (e.g. "_deliveryId") so the log output mirrors the model's public API.
|
|
333
|
+
function collectEntries(val) {
|
|
334
|
+
if (Array.isArray(val)) return val.map((v, i) => [i, v]);
|
|
335
|
+
|
|
336
|
+
const result = {};
|
|
337
|
+
const getterKeys = new Set();
|
|
338
|
+
|
|
339
|
+
// 1. Walk prototype chain and invoke getters
|
|
340
|
+
let proto = Object.getPrototypeOf(val);
|
|
341
|
+
while (proto && proto !== Object.prototype) {
|
|
342
|
+
const descs = Object.getOwnPropertyDescriptors(proto);
|
|
343
|
+
for (const [k, desc] of Object.entries(descs)) {
|
|
344
|
+
if (k === 'constructor') continue;
|
|
345
|
+
if (desc.get && !(k in result)) {
|
|
346
|
+
try { result[k] = desc.get.call(val); } catch { /* skip broken getters */ }
|
|
347
|
+
getterKeys.add(k);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
proto = Object.getPrototypeOf(proto);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2. Add own data properties, but skip backing fields whose getter is present.
|
|
354
|
+
// Convention: getter "foo" backs "_foo"; if "foo" was collected, skip "_foo".
|
|
355
|
+
const ownKeys = Object.keys(val);
|
|
356
|
+
for (const k of ownKeys) {
|
|
357
|
+
const clean = k.startsWith('_') ? k.slice(1) : null;
|
|
358
|
+
if (clean && getterKeys.has(clean)) continue; // skip _backing field
|
|
359
|
+
if (!(k in result)) result[k] = val[k];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return Object.entries(result);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function objPreview(val, maxLen) {
|
|
366
|
+
maxLen = maxLen || 80;
|
|
367
|
+
if (val === null) return 'null';
|
|
368
|
+
if (val === undefined) return 'undefined';
|
|
369
|
+
if (Array.isArray(val)) {
|
|
370
|
+
if (val.length === 0) return '[]';
|
|
371
|
+
const items = [];
|
|
372
|
+
let len = 2; // [ ]
|
|
373
|
+
for (let i = 0; i < val.length && len < maxLen; i++) {
|
|
374
|
+
const s = primitivePreview(val[i]);
|
|
375
|
+
len += s.length + 2;
|
|
376
|
+
items.push(s);
|
|
377
|
+
}
|
|
378
|
+
const suffix = items.length < val.length ? ', ...' : '';
|
|
379
|
+
return `(${val.length}) [${items.join(', ')}${suffix}]`;
|
|
380
|
+
}
|
|
381
|
+
if (typeof val === 'object') {
|
|
382
|
+
const entries = collectEntries(val);
|
|
383
|
+
if (entries.length === 0) return '{}';
|
|
384
|
+
const items = [];
|
|
385
|
+
let len = 2;
|
|
386
|
+
for (let i = 0; i < entries.length && len < maxLen; i++) {
|
|
387
|
+
const s = `${entries[i][0]}: ${primitivePreview(entries[i][1])}`;
|
|
388
|
+
len += s.length + 2;
|
|
389
|
+
items.push(s);
|
|
390
|
+
}
|
|
391
|
+
const suffix = items.length < entries.length ? ', ...' : '';
|
|
392
|
+
return `{${items.join(', ')}${suffix}}`;
|
|
393
|
+
}
|
|
394
|
+
return primitivePreview(val);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function primitivePreview(val) {
|
|
398
|
+
if (val === null) return 'null';
|
|
399
|
+
if (val === undefined) return 'undefined';
|
|
400
|
+
if (typeof val === 'string') return val.length > 50 ? `"${val.slice(0,50)}..."` : `"${val}"`;
|
|
401
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
402
|
+
if (Array.isArray(val)) return `Array(${val.length})`;
|
|
403
|
+
if (typeof val === 'object') return `{...}`;
|
|
404
|
+
return String(val);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function createTreeNode(key, val, startCollapsed) {
|
|
408
|
+
const isArray = Array.isArray(val);
|
|
409
|
+
const isObj = val !== null && typeof val === 'object';
|
|
410
|
+
|
|
411
|
+
if (!isObj) {
|
|
412
|
+
// Primitive leaf
|
|
413
|
+
const row = document.createElement('div');
|
|
414
|
+
row.className = 'ov-leaf';
|
|
415
|
+
if (key !== null) {
|
|
416
|
+
const k = document.createElement('span');
|
|
417
|
+
k.className = 'ov-key';
|
|
418
|
+
k.textContent = isNaN(key) ? `${key}: ` : `${key}: `;
|
|
419
|
+
row.appendChild(k);
|
|
420
|
+
}
|
|
421
|
+
row.appendChild(createPrimitiveSpan(val));
|
|
422
|
+
return row;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Collapsible object/array
|
|
426
|
+
const container = document.createElement('div');
|
|
427
|
+
container.className = 'ov-node';
|
|
428
|
+
|
|
429
|
+
const header = document.createElement('div');
|
|
430
|
+
header.className = 'ov-header';
|
|
431
|
+
|
|
432
|
+
const arrow = document.createElement('span');
|
|
433
|
+
arrow.className = 'ov-arrow';
|
|
434
|
+
arrow.textContent = '\u25B6'; // ▶
|
|
435
|
+
header.appendChild(arrow);
|
|
436
|
+
|
|
437
|
+
if (key !== null) {
|
|
438
|
+
const k = document.createElement('span');
|
|
439
|
+
k.className = 'ov-key';
|
|
440
|
+
k.textContent = `${key}: `;
|
|
441
|
+
header.appendChild(k);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const preview = document.createElement('span');
|
|
445
|
+
preview.className = 'ov-preview';
|
|
446
|
+
preview.textContent = objPreview(val);
|
|
447
|
+
header.appendChild(preview);
|
|
448
|
+
|
|
449
|
+
container.appendChild(header);
|
|
450
|
+
|
|
451
|
+
const children = document.createElement('div');
|
|
452
|
+
children.className = 'ov-children';
|
|
453
|
+
children.style.display = 'none';
|
|
454
|
+
|
|
455
|
+
let populated = false;
|
|
456
|
+
|
|
457
|
+
function populateChildren() {
|
|
458
|
+
if (populated) return;
|
|
459
|
+
populated = true;
|
|
460
|
+
const entries = collectEntries(val);
|
|
461
|
+
entries.forEach(([k, v]) => {
|
|
462
|
+
children.appendChild(createTreeNode(k, v, true));
|
|
463
|
+
});
|
|
464
|
+
// For arrays show length, for objects show prototype hint
|
|
465
|
+
if (isArray) {
|
|
466
|
+
const lenNode = document.createElement('div');
|
|
467
|
+
lenNode.className = 'ov-leaf ov-meta';
|
|
468
|
+
lenNode.textContent = `length: ${val.length}`;
|
|
469
|
+
children.appendChild(lenNode);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let expanded = !startCollapsed;
|
|
474
|
+
if (expanded) {
|
|
475
|
+
populateChildren();
|
|
476
|
+
children.style.display = 'block';
|
|
477
|
+
arrow.textContent = '\u25BC'; // ▼
|
|
478
|
+
arrow.classList.add('expanded');
|
|
479
|
+
preview.style.display = 'none';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
header.addEventListener('click', (e) => {
|
|
483
|
+
e.stopPropagation();
|
|
484
|
+
expanded = !expanded;
|
|
485
|
+
if (expanded) {
|
|
486
|
+
populateChildren();
|
|
487
|
+
children.style.display = 'block';
|
|
488
|
+
arrow.textContent = '\u25BC';
|
|
489
|
+
arrow.classList.add('expanded');
|
|
490
|
+
preview.style.display = 'none';
|
|
491
|
+
} else {
|
|
492
|
+
children.style.display = 'none';
|
|
493
|
+
arrow.textContent = '\u25B6';
|
|
494
|
+
arrow.classList.remove('expanded');
|
|
495
|
+
preview.style.display = '';
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
container.appendChild(children);
|
|
500
|
+
return container;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function _safeStr(val) {
|
|
504
|
+
if (val === null) return 'null';
|
|
505
|
+
if (val === undefined) return 'undefined';
|
|
506
|
+
if (typeof val === 'string') return val;
|
|
507
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
508
|
+
try { return JSON.stringify(val, null, 2); } catch { return String(val); }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function createPrimitiveSpan(val) {
|
|
512
|
+
const s = document.createElement('span');
|
|
513
|
+
if (val === null) { s.className = 'ov-null'; s.textContent = 'null'; }
|
|
514
|
+
else if (val === undefined) { s.className = 'ov-undef'; s.textContent = 'undefined'; }
|
|
515
|
+
else if (typeof val === 'string') { s.className = 'ov-str'; s.textContent = `"${val}"`; }
|
|
516
|
+
else if (typeof val === 'number') { s.className = 'ov-num'; s.textContent = String(val); }
|
|
517
|
+
else if (typeof val === 'boolean') { s.className = 'ov-bool'; s.textContent = String(val); }
|
|
518
|
+
else { s.textContent = _safeStr(val); }
|
|
519
|
+
return s;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Parse a structured arg from the SDK (or fall back to raw message string)
|
|
523
|
+
function renderConsoleArg(arg) {
|
|
524
|
+
if (!arg || typeof arg !== 'object' || !arg.t) {
|
|
525
|
+
// Backward compat: raw string
|
|
526
|
+
const s = document.createElement('span');
|
|
527
|
+
s.className = 'ov-str';
|
|
528
|
+
s.textContent = _safeStr(arg);
|
|
529
|
+
return s;
|
|
530
|
+
}
|
|
531
|
+
const { t, v } = arg;
|
|
532
|
+
if (t === 'string') {
|
|
533
|
+
const s = document.createElement('span');
|
|
534
|
+
s.className = 'log-text';
|
|
535
|
+
s.textContent = v;
|
|
536
|
+
return s;
|
|
537
|
+
}
|
|
538
|
+
if (t === 'number') { return createPrimitiveSpan(v); }
|
|
539
|
+
if (t === 'boolean') { return createPrimitiveSpan(v); }
|
|
540
|
+
if (t === 'null') { return createPrimitiveSpan(null); }
|
|
541
|
+
if (t === 'undefined') { return createPrimitiveSpan(undefined); }
|
|
542
|
+
if (t === 'object' || t === 'array') {
|
|
543
|
+
return createTreeNode(null, v, false);
|
|
544
|
+
}
|
|
545
|
+
const s = document.createElement('span');
|
|
546
|
+
s.textContent = _safeStr(v);
|
|
547
|
+
return s;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Build the body of a console log row. If structured args exist, render each;
|
|
551
|
+
// otherwise fall back to the flat message string and try to detect JSON in it.
|
|
552
|
+
function buildLogBody(logEntry) {
|
|
553
|
+
const container = document.createElement('div');
|
|
554
|
+
container.className = 'log-body';
|
|
555
|
+
|
|
556
|
+
if (logEntry.args && Array.isArray(logEntry.args) && logEntry.args.length > 0) {
|
|
557
|
+
// Structured args from updated SDK
|
|
558
|
+
logEntry.args.forEach((arg, i) => {
|
|
559
|
+
if (i > 0) container.appendChild(document.createTextNode(' '));
|
|
560
|
+
container.appendChild(renderConsoleArg(arg));
|
|
561
|
+
});
|
|
562
|
+
} else if (logEntry.message != null) {
|
|
563
|
+
// Legacy / flat message — try to parse JSON objects out of it
|
|
564
|
+
const msg = String(logEntry.message);
|
|
565
|
+
// Try parsing the whole message as JSON
|
|
566
|
+
try {
|
|
567
|
+
const parsed = JSON.parse(msg);
|
|
568
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
569
|
+
container.appendChild(createTreeNode(null, parsed, false));
|
|
570
|
+
return container;
|
|
571
|
+
}
|
|
572
|
+
} catch {}
|
|
573
|
+
|
|
574
|
+
// Otherwise render as text, but look for embedded JSON blocks
|
|
575
|
+
// If it looks like it contains JSON, try to pretty-render inline
|
|
576
|
+
const jsonRe = /(\{[\s\S]*\}|\[[\s\S]*\])/;
|
|
577
|
+
const match = msg.match(jsonRe);
|
|
578
|
+
if (match && match[0].length > 2) {
|
|
579
|
+
try {
|
|
580
|
+
const parsed = JSON.parse(match[0]);
|
|
581
|
+
// There's text before/after
|
|
582
|
+
const before = msg.slice(0, match.index);
|
|
583
|
+
const after = msg.slice(match.index + match[0].length);
|
|
584
|
+
if (before) container.appendChild(document.createTextNode(before));
|
|
585
|
+
container.appendChild(createTreeNode(null, parsed, false));
|
|
586
|
+
if (after) container.appendChild(document.createTextNode(after));
|
|
587
|
+
return container;
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Plain text
|
|
592
|
+
const span = document.createElement('span');
|
|
593
|
+
span.className = 'log-text';
|
|
594
|
+
span.textContent = msg;
|
|
595
|
+
container.appendChild(span);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return container;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function buildLogRow(l) {
|
|
602
|
+
const div = document.createElement('div');
|
|
603
|
+
div.className = `log-row entry ${l.level}`;
|
|
604
|
+
|
|
605
|
+
const timeSpan = document.createElement('span');
|
|
606
|
+
timeSpan.className = 'log-time';
|
|
607
|
+
timeSpan.textContent = ts(l.ts);
|
|
608
|
+
div.appendChild(timeSpan);
|
|
609
|
+
|
|
610
|
+
const lvlSpan = document.createElement('span');
|
|
611
|
+
lvlSpan.className = `lvl-badge lvl-${l.level}`;
|
|
612
|
+
lvlSpan.textContent = l.level;
|
|
613
|
+
div.appendChild(lvlSpan);
|
|
614
|
+
|
|
615
|
+
// Arrow (inline, not inside body-wrap)
|
|
616
|
+
const arrow = document.createElement('span');
|
|
617
|
+
arrow.className = 'log-arrow';
|
|
618
|
+
arrow.textContent = '\u25B6';
|
|
619
|
+
div.appendChild(arrow);
|
|
620
|
+
|
|
621
|
+
// Body wrapper
|
|
622
|
+
const bodyWrap = document.createElement('div');
|
|
623
|
+
bodyWrap.className = 'log-body-wrap';
|
|
624
|
+
|
|
625
|
+
// Single-line preview: message text + caller
|
|
626
|
+
const preview = document.createElement('div');
|
|
627
|
+
preview.className = 'log-preview';
|
|
628
|
+
const msgText = (l.message || '').replace(/\n/g, ' ').slice(0, 200);
|
|
629
|
+
const previewText = document.createElement('span');
|
|
630
|
+
previewText.textContent = msgText + ((l.message || '').length > 200 ? '...' : '');
|
|
631
|
+
preview.appendChild(previewText);
|
|
632
|
+
bodyWrap.appendChild(preview);
|
|
633
|
+
|
|
634
|
+
// Full content (hidden by default)
|
|
635
|
+
const full = document.createElement('div');
|
|
636
|
+
full.className = 'log-full';
|
|
637
|
+
full.style.display = 'none';
|
|
638
|
+
full.appendChild(buildLogBody(l));
|
|
639
|
+
bodyWrap.appendChild(full);
|
|
640
|
+
|
|
641
|
+
let expanded = false;
|
|
642
|
+
// Only toggle on click, NOT on text selection drag
|
|
643
|
+
let _mouseDownPos = null;
|
|
644
|
+
bodyWrap.addEventListener('mousedown', (e) => {
|
|
645
|
+
_mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
646
|
+
});
|
|
647
|
+
bodyWrap.addEventListener('click', (e) => {
|
|
648
|
+
// Don't toggle if user is selecting text (dragged mouse)
|
|
649
|
+
if (_mouseDownPos) {
|
|
650
|
+
const dx = Math.abs(e.clientX - _mouseDownPos.x);
|
|
651
|
+
const dy = Math.abs(e.clientY - _mouseDownPos.y);
|
|
652
|
+
if (dx > 3 || dy > 3) return; // user dragged to select
|
|
653
|
+
}
|
|
654
|
+
// Don't toggle if there's an active text selection
|
|
655
|
+
const sel = window.getSelection();
|
|
656
|
+
if (sel && sel.toString().length > 0) return;
|
|
657
|
+
// Don't toggle if clicking inside object tree expander
|
|
658
|
+
if (e.target.closest('.ov-header')) return;
|
|
659
|
+
expanded = !expanded;
|
|
660
|
+
if (expanded) {
|
|
661
|
+
preview.style.display = 'none';
|
|
662
|
+
full.style.display = 'block';
|
|
663
|
+
arrow.textContent = '\u25BC';
|
|
664
|
+
arrow.classList.add('expanded');
|
|
665
|
+
} else {
|
|
666
|
+
preview.style.display = '';
|
|
667
|
+
full.style.display = 'none';
|
|
668
|
+
arrow.textContent = '\u25B6';
|
|
669
|
+
arrow.classList.remove('expanded');
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Right-click → copy options
|
|
674
|
+
div.addEventListener('contextmenu', (e) => {
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
const items = [];
|
|
677
|
+
|
|
678
|
+
// Copy selected text
|
|
679
|
+
const sel = window.getSelection();
|
|
680
|
+
if (sel && sel.toString().length > 0) {
|
|
681
|
+
items.push({ label: 'Copy Selection', action: () => navigator.clipboard.writeText(sel.toString()) });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Copy full log message
|
|
685
|
+
items.push({ label: 'Copy Message', action: () => {
|
|
686
|
+
navigator.clipboard.writeText(l.message || '');
|
|
687
|
+
}});
|
|
688
|
+
|
|
689
|
+
// Copy as JSON (if structured args exist)
|
|
690
|
+
if (l.args && l.args.length > 0) {
|
|
691
|
+
items.push({ label: 'Copy as JSON', action: () => {
|
|
692
|
+
const json = l.args.map(a => {
|
|
693
|
+
if (a.t === 'object' || a.t === 'array') return JSON.stringify(a.v, null, 2);
|
|
694
|
+
return String(a.v);
|
|
695
|
+
}).join(' ');
|
|
696
|
+
navigator.clipboard.writeText(json);
|
|
697
|
+
}});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Copy caller location
|
|
701
|
+
if (l.caller) {
|
|
702
|
+
items.push({ label: 'Copy Caller', action: () => navigator.clipboard.writeText(l.caller) });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
showContextMenu(e, items);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
div.appendChild(bodyWrap);
|
|
709
|
+
return div;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ─── Shared context menu helper ──────────────────────────────────────────────
|
|
713
|
+
function showContextMenu(e, items) {
|
|
714
|
+
document.querySelectorAll('.ctx-menu').forEach(el => el.remove());
|
|
715
|
+
const menu = document.createElement('div');
|
|
716
|
+
menu.className = 'ctx-menu';
|
|
717
|
+
items.forEach(({ label, action }) => {
|
|
718
|
+
if (label === '—' || !action) {
|
|
719
|
+
const sep = document.createElement('div');
|
|
720
|
+
sep.className = 'ctx-sep';
|
|
721
|
+
menu.appendChild(sep);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const item = document.createElement('div');
|
|
725
|
+
item.className = 'ctx-item';
|
|
726
|
+
item.textContent = label;
|
|
727
|
+
item.addEventListener('click', () => { action(); menu.remove(); });
|
|
728
|
+
menu.appendChild(item);
|
|
729
|
+
});
|
|
730
|
+
menu.style.left = Math.min(e.clientX, window.innerWidth - 200) + 'px';
|
|
731
|
+
menu.style.top = Math.min(e.clientY, window.innerHeight - items.length * 32 - 10) + 'px';
|
|
732
|
+
document.body.appendChild(menu);
|
|
733
|
+
setTimeout(() => {
|
|
734
|
+
const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close); } };
|
|
735
|
+
document.addEventListener('click', close);
|
|
736
|
+
}, 0);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Full re-render — only used on filter/level change, NOT on every incoming log
|
|
740
|
+
function renderConsole() {
|
|
741
|
+
const list = $('consoleList');
|
|
742
|
+
const empty = $('consoleEmpty');
|
|
743
|
+
if (!list) return;
|
|
744
|
+
|
|
745
|
+
const { levelFilters, searchFilter } = state.console;
|
|
746
|
+
const visible = state.console.logs.filter(l => {
|
|
747
|
+
// Redux logs use showRedux flag; regular logs use levelFilters
|
|
748
|
+
if (l.level === 'redux') {
|
|
749
|
+
if (!state.console.showRedux) return false;
|
|
750
|
+
} else if (levelFilters && !levelFilters[l.level]) return false;
|
|
751
|
+
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return false;
|
|
752
|
+
return true;
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
list.querySelectorAll('.log-row').forEach(e => e.remove());
|
|
756
|
+
if (!empty) { /* guard */ }
|
|
757
|
+
else if (visible.length > 0) {
|
|
758
|
+
empty.style.display = 'none';
|
|
759
|
+
} else if (state.console.logs.length > 0) {
|
|
760
|
+
const lbl = empty.querySelector('.label');
|
|
761
|
+
const hint = empty.querySelector('.hint');
|
|
762
|
+
if (lbl) lbl.textContent = 'No matching logs';
|
|
763
|
+
if (hint) hint.textContent = 'Adjust level filters or clear search to see logs';
|
|
764
|
+
empty.style.display = 'flex';
|
|
765
|
+
} else {
|
|
766
|
+
const lbl = empty.querySelector('.label');
|
|
767
|
+
const hint = empty.querySelector('.hint');
|
|
768
|
+
if (lbl) lbl.textContent = 'No logs yet';
|
|
769
|
+
if (hint) hint.textContent = 'Logs will appear here automatically';
|
|
770
|
+
empty.style.display = 'flex';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Render only the last N visible rows for performance
|
|
774
|
+
const MAX_RENDER = 5000;
|
|
775
|
+
const toRender = visible.length > MAX_RENDER ? visible.slice(-MAX_RENDER) : visible;
|
|
776
|
+
if (visible.length > MAX_RENDER) {
|
|
777
|
+
const info = document.createElement('div');
|
|
778
|
+
info.className = 'log-row';
|
|
779
|
+
info.style.cssText = 'color:var(--text-dim);font-size:10px;padding:6px 14px;text-align:center;font-style:italic';
|
|
780
|
+
info.textContent = `${visible.length - MAX_RENDER} older logs hidden for performance`;
|
|
781
|
+
list.appendChild(info);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const frag = document.createDocumentFragment();
|
|
785
|
+
toRender.forEach(l => frag.appendChild(buildLogRow(l)));
|
|
786
|
+
list.appendChild(frag);
|
|
787
|
+
list.scrollTop = list.scrollHeight;
|
|
788
|
+
}
|
|
789
|
+
|