react-native-debug-toolkit 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/lib/commonjs/core/DebugToolkit.js +118 -97
- package/lib/commonjs/core/DebugToolkit.js.map +1 -1
- package/lib/commonjs/core/initialize.js +4 -4
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/environment/index.js +22 -24
- package/lib/commonjs/features/environment/index.js.map +1 -1
- package/lib/commonjs/features/network/index.js +25 -47
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +3 -3
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +0 -30
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/DaemonClient.js +13 -51
- package/lib/commonjs/utils/DaemonClient.js.map +1 -1
- package/lib/commonjs/utils/createChannelFeature.js +8 -1
- package/lib/commonjs/utils/createChannelFeature.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +2 -1
- package/lib/commonjs/utils/deviceReport.js.map +1 -1
- package/lib/commonjs/utils/urlRewriter.js +15 -0
- package/lib/commonjs/utils/urlRewriter.js.map +1 -0
- package/lib/module/core/DebugToolkit.js +117 -96
- package/lib/module/core/DebugToolkit.js.map +1 -1
- package/lib/module/core/initialize.js +6 -7
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/environment/index.js +22 -24
- package/lib/module/features/environment/index.js.map +1 -1
- package/lib/module/features/network/index.js +25 -46
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +3 -3
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/DaemonClient.js +14 -42
- package/lib/module/utils/DaemonClient.js.map +1 -1
- package/lib/module/utils/createChannelFeature.js +8 -1
- package/lib/module/utils/createChannelFeature.js.map +1 -1
- package/lib/module/utils/deviceReport.js +3 -2
- package/lib/module/utils/deviceReport.js.map +1 -1
- package/lib/module/utils/urlRewriter.js +10 -0
- package/lib/module/utils/urlRewriter.js.map +1 -0
- package/lib/typescript/src/core/DebugToolkit.d.ts +23 -10
- package/lib/typescript/src/core/DebugToolkit.d.ts.map +1 -1
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/environment/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/index.d.ts +3 -3
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/feature.d.ts +5 -0
- package/lib/typescript/src/types/feature.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +1 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/DaemonClient.d.ts +4 -11
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
- package/lib/typescript/src/utils/createChannelFeature.d.ts +4 -0
- package/lib/typescript/src/utils/createChannelFeature.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +4 -1
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
- package/lib/typescript/src/utils/urlRewriter.d.ts +5 -0
- package/lib/typescript/src/utils/urlRewriter.d.ts.map +1 -0
- package/node/daemon/src/console/console.html +324 -168
- package/node/daemon/src/server.js +32 -2
- package/node/mcp/src/logs.js +15 -4
- package/node/mcp/src/tools.js +4 -2
- package/package.json +1 -1
- package/src/core/DebugToolkit.tsx +119 -105
- package/src/core/initialize.ts +7 -8
- package/src/features/environment/index.ts +25 -27
- package/src/features/network/index.ts +30 -52
- package/src/features/network/networkInterceptor.ts +3 -3
- package/src/index.ts +3 -8
- package/src/types/feature.ts +6 -0
- package/src/types/index.ts +1 -0
- package/src/utils/DaemonClient.ts +14 -56
- package/src/utils/createChannelFeature.ts +12 -1
- package/src/utils/deviceReport.ts +5 -3
- package/src/utils/urlRewriter.ts +11 -0
- package/lib/commonjs/utils/urlRewriterRegistry.js +0 -14
- package/lib/commonjs/utils/urlRewriterRegistry.js.map +0 -1
- package/lib/module/utils/urlRewriterRegistry.js +0 -10
- package/lib/module/utils/urlRewriterRegistry.js.map +0 -1
- package/lib/typescript/src/utils/urlRewriterRegistry.d.ts +0 -7
- package/lib/typescript/src/utils/urlRewriterRegistry.d.ts.map +0 -1
- package/src/utils/urlRewriterRegistry.ts +0 -10
|
@@ -179,12 +179,6 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
179
179
|
font-size:12px;color:var(--text2);display:flex;align-items:center;gap:6px;
|
|
180
180
|
font-family:var(--font-mono);cursor:pointer;
|
|
181
181
|
}
|
|
182
|
-
.toolbar input[type=number]{
|
|
183
|
-
width:56px;padding:4px 8px;
|
|
184
|
-
background:var(--surface);border:1px solid var(--border2);border-radius:4px;
|
|
185
|
-
color:var(--text);font-size:11px;font-family:var(--font-mono);
|
|
186
|
-
}
|
|
187
|
-
.toolbar input[type=number]:focus{outline:none;border-color:var(--cyan)}
|
|
188
182
|
.toggle{
|
|
189
183
|
position:relative;width:32px;height:18px;
|
|
190
184
|
background:var(--surface3);border-radius:9px;cursor:pointer;
|
|
@@ -227,6 +221,26 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
227
221
|
background:var(--surface2);border:1px solid var(--border2);
|
|
228
222
|
padding:1px 4px;border-radius:2px;letter-spacing:.02em;
|
|
229
223
|
}
|
|
224
|
+
.pager{
|
|
225
|
+
display:flex;align-items:center;justify-content:flex-end;gap:8px;
|
|
226
|
+
flex-wrap:wrap;color:var(--text3);font-family:var(--font-mono);font-size:11px;
|
|
227
|
+
}
|
|
228
|
+
.toolbar .pager{margin-left:auto}
|
|
229
|
+
.pager-bottom{margin-top:14px}
|
|
230
|
+
.pager-info{white-space:nowrap}
|
|
231
|
+
.page-btn{
|
|
232
|
+
padding:4px 9px;border:1px solid var(--border2);border-radius:4px;
|
|
233
|
+
background:var(--surface);color:var(--text2);font-family:var(--font-mono);
|
|
234
|
+
font-size:11px;cursor:pointer;
|
|
235
|
+
}
|
|
236
|
+
.page-btn:hover:not(:disabled){color:var(--cyan);border-color:var(--cyan-mid)}
|
|
237
|
+
.page-btn:disabled{opacity:.4;cursor:not-allowed}
|
|
238
|
+
.live-notice{
|
|
239
|
+
display:none;padding:4px 9px;border:1px solid var(--cyan-mid);border-radius:4px;
|
|
240
|
+
background:var(--cyan-dim);color:var(--cyan);font-family:var(--font-mono);
|
|
241
|
+
font-size:11px;cursor:pointer;
|
|
242
|
+
}
|
|
243
|
+
.live-notice.visible{display:inline-flex}
|
|
230
244
|
|
|
231
245
|
/* Curl help - collapsible */
|
|
232
246
|
.curl-panel{
|
|
@@ -407,6 +421,33 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
407
421
|
padding:12px 14px;border-bottom:1px solid rgba(30,45,74,.5);
|
|
408
422
|
display:flex;align-items:center;gap:10px;
|
|
409
423
|
}
|
|
424
|
+
.network-meta-line{
|
|
425
|
+
padding:6px 14px;border-bottom:1px solid rgba(30,45,74,.5);
|
|
426
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text3);
|
|
427
|
+
display:flex;gap:10px;flex-wrap:wrap;
|
|
428
|
+
}
|
|
429
|
+
.network-meta-line span{white-space:nowrap}
|
|
430
|
+
|
|
431
|
+
/* Collapsible sections */
|
|
432
|
+
.collapse-section{border:1px solid var(--border);border-radius:var(--radius);background:rgba(8,12,22,.35);overflow:hidden;margin-bottom:0}
|
|
433
|
+
.collapse-header{
|
|
434
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
435
|
+
padding:7px 12px;border-bottom:1px solid var(--border);
|
|
436
|
+
background:rgba(0,229,255,.03);cursor:pointer;user-select:none;
|
|
437
|
+
}
|
|
438
|
+
.collapse-header:hover{background:rgba(0,229,255,.06)}
|
|
439
|
+
.collapse-header-left{display:flex;align-items:center;gap:6px}
|
|
440
|
+
.collapse-arrow{
|
|
441
|
+
font-size:9px;color:var(--text3);transition:transform .2s;display:inline-block;
|
|
442
|
+
}
|
|
443
|
+
.collapse-arrow.open{transform:rotate(90deg)}
|
|
444
|
+
.collapse-title{
|
|
445
|
+
font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
|
|
446
|
+
color:var(--cyan);font-family:var(--font-mono);
|
|
447
|
+
}
|
|
448
|
+
.collapse-body{display:none;padding:0}
|
|
449
|
+
.collapse-body.open{display:block}
|
|
450
|
+
.section-body-inner{padding:4px 0}
|
|
410
451
|
.method-badge{
|
|
411
452
|
font-family:var(--font-mono);font-size:11px;font-weight:700;
|
|
412
453
|
padding:3px 10px;border-radius:3px;letter-spacing:.04em;
|
|
@@ -540,6 +581,10 @@ mark{
|
|
|
540
581
|
var authToken = null;
|
|
541
582
|
var searchTerm = '';
|
|
542
583
|
var focusedIndex = -1;
|
|
584
|
+
var PAGE_SIZE = 200;
|
|
585
|
+
var currentPage = 1;
|
|
586
|
+
var pendingLiveCount = 0;
|
|
587
|
+
var liveSequence = 0;
|
|
543
588
|
|
|
544
589
|
try {
|
|
545
590
|
var params = new URLSearchParams(location.search);
|
|
@@ -801,6 +846,34 @@ mark{
|
|
|
801
846
|
return renderSection(title, renderValue(object), object);
|
|
802
847
|
}
|
|
803
848
|
|
|
849
|
+
function renderCollapsibleSection(title, content, dataForCopy, collapsed) {
|
|
850
|
+
var id = 'ns-' + Math.random().toString(36).slice(2,8);
|
|
851
|
+
var isOpen = !collapsed;
|
|
852
|
+
var copyAttr = dataForCopy !== undefined
|
|
853
|
+
? ' data-copy="' + escapeHtml(typeof dataForCopy === 'string' ? dataForCopy : JSON.stringify(dataForCopy)) + '"'
|
|
854
|
+
: '';
|
|
855
|
+
var html = '<div class="collapse-section">';
|
|
856
|
+
html += '<div class="collapse-header" onclick="toggleNetworkSection(\'' + id + '\')">';
|
|
857
|
+
html += '<div class="collapse-header-left">';
|
|
858
|
+
html += '<span class="collapse-arrow' + (isOpen ? ' open' : '') + '" id="arrow-' + id + '">▶</span>';
|
|
859
|
+
html += '<span class="collapse-title">' + escapeHtml(title) + '</span>';
|
|
860
|
+
html += '</div>';
|
|
861
|
+
html += '<button class="detail-section-copy" onclick="event.stopPropagation();copySectionData(this)" title="Copy section"' + copyAttr + '>⎘</button>';
|
|
862
|
+
html += '</div>';
|
|
863
|
+
html += '<div class="collapse-body' + (isOpen ? ' open' : '') + '" id="' + id + '">';
|
|
864
|
+
html += '<div class="section-body-inner">' + content + '</div>';
|
|
865
|
+
html += '</div></div>';
|
|
866
|
+
return html;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
window.toggleNetworkSection = function(id) {
|
|
870
|
+
var body = document.getElementById(id);
|
|
871
|
+
var arrow = document.getElementById('arrow-' + id);
|
|
872
|
+
if (!body) return;
|
|
873
|
+
body.classList.toggle('open');
|
|
874
|
+
if (arrow) arrow.classList.toggle('open');
|
|
875
|
+
};
|
|
876
|
+
|
|
804
877
|
function renderConsoleDetails(entry) {
|
|
805
878
|
var messages = Array.isArray(entry.data) ? entry.data : [entry.data];
|
|
806
879
|
return renderSection('Console', renderRows([
|
|
@@ -817,30 +890,53 @@ mark{
|
|
|
817
890
|
var method = (request.method || 'GET').toUpperCase();
|
|
818
891
|
var methodClass = 'method-' + method.toLowerCase();
|
|
819
892
|
|
|
820
|
-
|
|
893
|
+
// Hero - always visible
|
|
894
|
+
var html = '<div class="network-hero"><span class="method-badge ' + methodClass + '">' + escapeHtml(method) + '</span>' +
|
|
821
895
|
'<span class="network-url">' + escapeHtml(request.url || '-') + '</span></div>';
|
|
822
896
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (
|
|
897
|
+
// Compact meta line - always visible
|
|
898
|
+
var metaParts = [];
|
|
899
|
+
if (response && response.status !== undefined) {
|
|
900
|
+
metaParts.push(response.status + (response.statusText ? ' ' + response.statusText : ''));
|
|
901
|
+
}
|
|
902
|
+
if (entry.duration !== undefined) metaParts.push(entry.duration + 'ms');
|
|
903
|
+
if (entry.timestamp) metaParts.push(formatTimeShort(new Date(entry.timestamp).toISOString()));
|
|
904
|
+
if (metaParts.length) {
|
|
905
|
+
html += '<div class="network-meta-line">' + metaParts.map(function(p) { return '<span>' + escapeHtml(p) + '</span>'; }).join('') + '</div>';
|
|
906
|
+
}
|
|
829
907
|
|
|
830
|
-
|
|
908
|
+
// Request Body - EXPANDED by default, always visible
|
|
909
|
+
var reqBodyContent = request.body
|
|
910
|
+
? renderValue(request.body)
|
|
911
|
+
: '<span class="value-pill" style="color:var(--text3)">No request body</span>';
|
|
912
|
+
html += renderCollapsibleSection('Request Body', reqBodyContent, request.body || null, false);
|
|
831
913
|
|
|
914
|
+
// Response Data - EXPANDED by default, always visible
|
|
832
915
|
if (response) {
|
|
833
|
-
var
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (response.headers) resRows.push(['Headers', response.headers]);
|
|
838
|
-
if (response.data) resRows.push(['Data', response.data]);
|
|
839
|
-
html += renderSection('Response', renderRows(resRows), response);
|
|
916
|
+
var resDataContent = response.data != null
|
|
917
|
+
? renderValue(response.data)
|
|
918
|
+
: '<span class="value-pill" style="color:var(--text3)">No response body</span>';
|
|
919
|
+
html += renderCollapsibleSection('Response Data', resDataContent, response.data || null, false);
|
|
840
920
|
}
|
|
921
|
+
|
|
922
|
+
// Error - always visible if present
|
|
841
923
|
if (entry.error) {
|
|
842
924
|
html += renderSection('Error', renderValue(entry.error), entry.error);
|
|
843
925
|
}
|
|
926
|
+
|
|
927
|
+
// Everything below is COLLAPSED by default
|
|
928
|
+
var reqHeadersContent = request.headers
|
|
929
|
+
? renderValue(request.headers)
|
|
930
|
+
: '<span class="value-pill" style="color:var(--text3)">No headers</span>';
|
|
931
|
+
html += renderCollapsibleSection('Request Headers', reqHeadersContent, request.headers || null, true);
|
|
932
|
+
|
|
933
|
+
if (response) {
|
|
934
|
+
var resHeadersContent = response.headers
|
|
935
|
+
? renderValue(response.headers)
|
|
936
|
+
: '<span class="value-pill" style="color:var(--text3)">No headers</span>';
|
|
937
|
+
html += renderCollapsibleSection('Response Headers', resHeadersContent, response.headers || null, true);
|
|
938
|
+
}
|
|
939
|
+
|
|
844
940
|
return html;
|
|
845
941
|
}
|
|
846
942
|
|
|
@@ -960,7 +1056,7 @@ mark{
|
|
|
960
1056
|
var lc = deviceLog.logCount || {};
|
|
961
1057
|
var deviceText = formatDevice(deviceLog.device);
|
|
962
1058
|
var ipText = formatIp(deviceLog.source);
|
|
963
|
-
html += '<div class="device-card" data-device-id="' + escapeHtml(deviceLog.deviceId) + '" style="animation-delay:' + (i * 40) + 'ms"
|
|
1059
|
+
html += '<div class="device-card" data-device-id="' + escapeHtml(deviceLog.deviceId) + '" style="animation-delay:' + (i * 40) + 'ms">';
|
|
964
1060
|
html += '<div><div class="device-title">' + escapeHtml(deviceText) + '</div>';
|
|
965
1061
|
html += '<div class="device-subtitle">IP ' + escapeHtml(ipText) + '</div></div>';
|
|
966
1062
|
html += '<div class="device-meta-group">';
|
|
@@ -985,6 +1081,7 @@ mark{
|
|
|
985
1081
|
expandedRows = {};
|
|
986
1082
|
searchTerm = '';
|
|
987
1083
|
focusedIndex = -1;
|
|
1084
|
+
currentPage = 1;
|
|
988
1085
|
window._currentFilterType = '';
|
|
989
1086
|
window._failedOnly = false;
|
|
990
1087
|
statusEl.textContent = 'loading...';
|
|
@@ -1000,7 +1097,7 @@ mark{
|
|
|
1000
1097
|
var html = '';
|
|
1001
1098
|
|
|
1002
1099
|
// Back link
|
|
1003
|
-
html += '<a href="#" class="back-link"
|
|
1100
|
+
html += '<a href="#" class="back-link">';
|
|
1004
1101
|
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>';
|
|
1005
1102
|
html += 'All devices</a>';
|
|
1006
1103
|
|
|
@@ -1038,9 +1135,10 @@ mark{
|
|
|
1038
1135
|
|
|
1039
1136
|
html += renderCurlPanel('Curl this device', [
|
|
1040
1137
|
curlCommand('/devices/' + encodeURIComponent(deviceId)),
|
|
1041
|
-
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?limit=
|
|
1138
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?limit=200'),
|
|
1139
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?limit=200&includeBodies=true'),
|
|
1042
1140
|
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?type=network&failedOnly=true&limit=50'),
|
|
1043
|
-
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?type=console&limit=
|
|
1141
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?type=console&limit=200'),
|
|
1044
1142
|
]);
|
|
1045
1143
|
|
|
1046
1144
|
// Tabs
|
|
@@ -1060,11 +1158,13 @@ mark{
|
|
|
1060
1158
|
html += '<button class="search-clear" id="searchClear" onclick="clearSearch()">×</button>';
|
|
1061
1159
|
html += '</div>';
|
|
1062
1160
|
html += '<label>Failed only <div class="toggle" id="failedToggle" onclick="toggleFailed()"></div></label>';
|
|
1063
|
-
html += '<
|
|
1161
|
+
html += '<button class="live-notice" id="liveNotice" onclick="showLiveUpdates()">0 new logs</button>';
|
|
1162
|
+
html += '<div class="pager" id="pagerTop"></div>';
|
|
1064
1163
|
html += '</div>';
|
|
1065
1164
|
|
|
1066
1165
|
// Log container
|
|
1067
1166
|
html += '<div id="logsContainer"></div>';
|
|
1167
|
+
html += '<div class="pager pager-bottom" id="pagerBottom"></div>';
|
|
1068
1168
|
|
|
1069
1169
|
// Actions
|
|
1070
1170
|
html += '<div class="actions">';
|
|
@@ -1086,15 +1186,14 @@ mark{
|
|
|
1086
1186
|
applyFilters();
|
|
1087
1187
|
});
|
|
1088
1188
|
|
|
1089
|
-
|
|
1090
|
-
renderLogs(logs, '', 50, false);
|
|
1189
|
+
renderLogs(logs, '', false);
|
|
1091
1190
|
}).catch(function(err) {
|
|
1092
1191
|
statusEl.textContent = '';
|
|
1093
1192
|
app.innerHTML = '<div class="empty" style="color:var(--red)">Failed to load: ' + escapeHtml(err.message) + '</div>';
|
|
1094
1193
|
});
|
|
1095
1194
|
}
|
|
1096
1195
|
|
|
1097
|
-
function
|
|
1196
|
+
function collectLogEntries(logs, type, failedOnly) {
|
|
1098
1197
|
var entries = [];
|
|
1099
1198
|
if (type && logs[type]) {
|
|
1100
1199
|
entries = Array.isArray(logs[type])
|
|
@@ -1126,8 +1225,63 @@ mark{
|
|
|
1126
1225
|
.sort(function(a, b) {
|
|
1127
1226
|
var byTime = readTimestamp(b.entry) - readTimestamp(a.entry);
|
|
1128
1227
|
return byTime || b.order - a.order;
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
return entries;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function renderPagination(total, page, pageSize) {
|
|
1234
|
+
var totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
1235
|
+
var start = total === 0 ? 0 : ((page - 1) * pageSize) + 1;
|
|
1236
|
+
var end = Math.min(page * pageSize, total);
|
|
1237
|
+
var html = '<span class="pager-info">' + start + '-' + end + ' / ' + total + ' · ' + pageSize + ' per page</span>';
|
|
1238
|
+
html += '<button class="page-btn" onclick="goToPage(' + (page - 1) + ')"' + (page <= 1 ? ' disabled' : '') + '>Prev</button>';
|
|
1239
|
+
html += '<span class="pager-info">' + page + ' / ' + totalPages + '</span>';
|
|
1240
|
+
html += '<button class="page-btn" onclick="goToPage(' + (page + 1) + ')"' + (page >= totalPages ? ' disabled' : '') + '>Next</button>';
|
|
1241
|
+
|
|
1242
|
+
['pagerTop', 'pagerBottom'].forEach(function(id) {
|
|
1243
|
+
var el = document.getElementById(id);
|
|
1244
|
+
if (el) el.innerHTML = html;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function renderLogEntryHtml(entry, type, rowId, index, isExpanded) {
|
|
1249
|
+
var lt = type || getLogType(entry);
|
|
1250
|
+
var typeClass = toKeyPart(lt);
|
|
1251
|
+
var ts = readTimestamp(entry);
|
|
1252
|
+
var html = '<div class="log-entry' + (isExpanded ? ' expanded' : '') + '" id="entry-' + rowId + '" data-index="' + index + '" data-sort="' + ts + '">';
|
|
1253
|
+
html += '<div class="log-row" onclick="toggleRow(\'' + rowId + '\')">';
|
|
1254
|
+
html += '<div class="log-type log-type-' + typeClass + '">' + escapeHtml(labelForType(lt)) + '</div>';
|
|
1255
|
+
html += '<div class="log-summary-col">';
|
|
1256
|
+
html += '<div class="log-summary">' + matchSearch(summarize(entry)) + '</div>';
|
|
1257
|
+
if (ts) {
|
|
1258
|
+
html += '<div class="log-timestamp">' + formatTimeShort(new Date(ts).toISOString()) + '</div>';
|
|
1259
|
+
}
|
|
1260
|
+
html += '</div>';
|
|
1261
|
+
html += '<div class="log-status">' + statusBadge(entry) + '</div>';
|
|
1262
|
+
html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">⎘</button></div>';
|
|
1263
|
+
html += '<div class="log-expand">' + (isExpanded ? '▶' : '▶') + '</div>';
|
|
1264
|
+
html += '</div>';
|
|
1265
|
+
html += '<div class="log-detail' + (isExpanded ? '' : '') + '" id="detail-' + rowId + '">';
|
|
1266
|
+
html += '<div class="log-detail-inner"><div class="detail-sections">';
|
|
1267
|
+
html += renderLogDetails(entry, lt);
|
|
1268
|
+
html += '<div class="entry-footer">';
|
|
1269
|
+
html += '<button class="btn btn-sm" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')">⎘ Copy JSON</button>';
|
|
1270
|
+
html += '</div>';
|
|
1271
|
+
html += '</div></div></div>';
|
|
1272
|
+
html += '</div>';
|
|
1273
|
+
return html;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function renderLogs(logs, type, failedOnly) {
|
|
1277
|
+
var allEntries = collectLogEntries(logs, type, failedOnly);
|
|
1278
|
+
var totalPages = Math.max(1, Math.ceil(allEntries.length / PAGE_SIZE));
|
|
1279
|
+
if (currentPage > totalPages) currentPage = totalPages;
|
|
1280
|
+
if (currentPage < 1) currentPage = 1;
|
|
1281
|
+
|
|
1282
|
+
var startIndex = (currentPage - 1) * PAGE_SIZE;
|
|
1283
|
+
var entries = allEntries.slice(startIndex, startIndex + PAGE_SIZE);
|
|
1284
|
+
renderPagination(allEntries.length, currentPage, PAGE_SIZE);
|
|
1131
1285
|
|
|
1132
1286
|
var container = document.getElementById('logsContainer');
|
|
1133
1287
|
if (!entries.length) {
|
|
@@ -1140,37 +1294,9 @@ mark{
|
|
|
1140
1294
|
focusedIndex = -1;
|
|
1141
1295
|
var html = '<div class="log-list">';
|
|
1142
1296
|
entries.forEach(function(item, i) {
|
|
1143
|
-
var
|
|
1144
|
-
var rowId = getLogEntryKey(entry, item.type,
|
|
1145
|
-
|
|
1146
|
-
var typeClass = toKeyPart(lt);
|
|
1147
|
-
var isExpanded = expandedRows[rowId];
|
|
1148
|
-
var ts = readTimestamp(entry);
|
|
1149
|
-
|
|
1150
|
-
html += '<div class="log-entry' + (isExpanded ? ' expanded' : '') + '" id="entry-' + rowId + '" data-index="' + i + '">';
|
|
1151
|
-
html += '<div class="log-row" onclick="toggleRow(\'' + rowId + '\')">';
|
|
1152
|
-
html += '<div class="log-type log-type-' + typeClass + '">' + escapeHtml(labelForType(lt)) + '</div>';
|
|
1153
|
-
html += '<div class="log-summary-col">';
|
|
1154
|
-
html += '<div class="log-summary">' + matchSearch(summarize(entry)) + '</div>';
|
|
1155
|
-
if (ts) {
|
|
1156
|
-
html += '<div class="log-timestamp">' + formatTimeShort(new Date(ts).toISOString()) + '</div>';
|
|
1157
|
-
}
|
|
1158
|
-
html += '</div>';
|
|
1159
|
-
html += '<div class="log-status">' + statusBadge(entry) + '</div>';
|
|
1160
|
-
html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">⎘</button></div>';
|
|
1161
|
-
html += '<div class="log-expand">' + (isExpanded ? '▶' : '▶') + '</div>';
|
|
1162
|
-
html += '</div>';
|
|
1163
|
-
|
|
1164
|
-
// Detail panel
|
|
1165
|
-
html += '<div class="log-detail' + (isExpanded ? '' : '') + '" id="detail-' + rowId + '">';
|
|
1166
|
-
html += '<div class="log-detail-inner"><div class="detail-sections">';
|
|
1167
|
-
html += renderLogDetails(entry, lt);
|
|
1168
|
-
html += '<div class="entry-footer">';
|
|
1169
|
-
html += '<button class="btn btn-sm" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')">⎘ Copy JSON</button>';
|
|
1170
|
-
html += '</div>';
|
|
1171
|
-
html += '</div></div></div>';
|
|
1172
|
-
|
|
1173
|
-
html += '</div>';
|
|
1297
|
+
var absoluteIndex = startIndex + i;
|
|
1298
|
+
var rowId = getLogEntryKey(item.entry, item.type, absoluteIndex);
|
|
1299
|
+
html += renderLogEntryHtml(item.entry, item.type, rowId, i, expandedRows[rowId]);
|
|
1174
1300
|
});
|
|
1175
1301
|
html += '</div>';
|
|
1176
1302
|
container.innerHTML = html;
|
|
@@ -1182,7 +1308,7 @@ mark{
|
|
|
1182
1308
|
return {
|
|
1183
1309
|
type: window._currentFilterType || '',
|
|
1184
1310
|
failedOnly: window._failedOnly || false,
|
|
1185
|
-
|
|
1311
|
+
page: currentPage,
|
|
1186
1312
|
};
|
|
1187
1313
|
}
|
|
1188
1314
|
|
|
@@ -1190,7 +1316,27 @@ mark{
|
|
|
1190
1316
|
if (!currentDevice) return;
|
|
1191
1317
|
var logs = currentDevice.report ? currentDevice.report.logs : {};
|
|
1192
1318
|
var options = readVisibleLogOptions();
|
|
1193
|
-
renderLogs(logs, options.type, options.
|
|
1319
|
+
renderLogs(logs, options.type, options.failedOnly);
|
|
1320
|
+
updateLiveNotice();
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function updateCurrentPagination() {
|
|
1324
|
+
if (!currentDevice) return;
|
|
1325
|
+
var logs = currentDevice.report ? currentDevice.report.logs : {};
|
|
1326
|
+
var options = readVisibleLogOptions();
|
|
1327
|
+
var total = collectLogEntries(logs, options.type, options.failedOnly).length;
|
|
1328
|
+
renderPagination(total, currentPage, PAGE_SIZE);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function updateLiveNotice() {
|
|
1332
|
+
var notice = document.getElementById('liveNotice');
|
|
1333
|
+
if (!notice) return;
|
|
1334
|
+
if (pendingLiveCount > 0) {
|
|
1335
|
+
notice.textContent = pendingLiveCount + ' new logs';
|
|
1336
|
+
notice.classList.add('visible');
|
|
1337
|
+
} else {
|
|
1338
|
+
notice.classList.remove('visible');
|
|
1339
|
+
}
|
|
1194
1340
|
}
|
|
1195
1341
|
|
|
1196
1342
|
function refreshCurrentDevice() {
|
|
@@ -1207,6 +1353,7 @@ mark{
|
|
|
1207
1353
|
currentDevice.logCount = data.logCount;
|
|
1208
1354
|
currentDevice.receivedAt = data.receivedAt;
|
|
1209
1355
|
currentDevice.lastSeenAt = data.lastSeenAt;
|
|
1356
|
+
pendingLiveCount = 0;
|
|
1210
1357
|
rerenderVisibleLogs();
|
|
1211
1358
|
updateTabCounts();
|
|
1212
1359
|
}).catch(function(err) {
|
|
@@ -1238,6 +1385,24 @@ mark{
|
|
|
1238
1385
|
window.applyFilters = function() {
|
|
1239
1386
|
if (!currentDevice) return;
|
|
1240
1387
|
expandedRows = {};
|
|
1388
|
+
currentPage = 1;
|
|
1389
|
+
pendingLiveCount = 0;
|
|
1390
|
+
rerenderVisibleLogs();
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
window.goToPage = function(page) {
|
|
1394
|
+
if (!currentDevice) return;
|
|
1395
|
+
currentPage = Math.max(1, Math.floor(page));
|
|
1396
|
+
expandedRows = {};
|
|
1397
|
+
if (currentPage === 1) pendingLiveCount = 0;
|
|
1398
|
+
rerenderVisibleLogs();
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
window.showLiveUpdates = function() {
|
|
1402
|
+
if (!currentDevice) return;
|
|
1403
|
+
currentPage = 1;
|
|
1404
|
+
expandedRows = {};
|
|
1405
|
+
pendingLiveCount = 0;
|
|
1241
1406
|
rerenderVisibleLogs();
|
|
1242
1407
|
};
|
|
1243
1408
|
|
|
@@ -1294,33 +1459,11 @@ mark{
|
|
|
1294
1459
|
var el = document.getElementById('entry-' + rowId);
|
|
1295
1460
|
if (!el) return;
|
|
1296
1461
|
|
|
1297
|
-
// Re-derive the entry from current logs
|
|
1298
1462
|
var logs = currentDevice.report ? currentDevice.report.logs : {};
|
|
1299
|
-
var entries = [];
|
|
1300
|
-
Object.entries(logs).forEach(function(logGroup) {
|
|
1301
|
-
var logType = logGroup[0];
|
|
1302
|
-
var value = logGroup[1];
|
|
1303
|
-
if (Array.isArray(value)) {
|
|
1304
|
-
value.forEach(function(entry, index) {
|
|
1305
|
-
entries.push({ type: logType, entry: entry, order: entries.length });
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
1463
|
var options = readVisibleLogOptions();
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
if (options.failedOnly) {
|
|
1315
|
-
entries = entries.filter(function(item) { return isFailedEntry(item.entry); });
|
|
1316
|
-
}
|
|
1317
|
-
if (searchTerm) {
|
|
1318
|
-
entries = entries.filter(function(item) { return entryMatchesSearch(item.entry); });
|
|
1319
|
-
}
|
|
1320
|
-
entries = entries.slice().sort(function(a, b) {
|
|
1321
|
-
var byTime = readTimestamp(b.entry) - readTimestamp(a.entry);
|
|
1322
|
-
return byTime || b.order - a.order;
|
|
1323
|
-
}).slice(0, options.limit);
|
|
1464
|
+
var startIndex = (options.page - 1) * PAGE_SIZE;
|
|
1465
|
+
var entries = collectLogEntries(logs, options.type, options.failedOnly)
|
|
1466
|
+
.slice(startIndex, startIndex + PAGE_SIZE);
|
|
1324
1467
|
|
|
1325
1468
|
var idx = parseInt(el.getAttribute('data-index'), 10);
|
|
1326
1469
|
if (isNaN(idx) || idx < 0 || idx >= entries.length) return;
|
|
@@ -1417,6 +1560,25 @@ mark{
|
|
|
1417
1560
|
items[focusedIndex].click();
|
|
1418
1561
|
}
|
|
1419
1562
|
|
|
1563
|
+
function openDeviceDetail(deviceId) {
|
|
1564
|
+
if (!deviceId) return;
|
|
1565
|
+
var nextHash = 'device/' + encodeURIComponent(deviceId);
|
|
1566
|
+
if (location.hash === '#' + nextHash) {
|
|
1567
|
+
route();
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
location.hash = nextHash;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
document.addEventListener('click', function(e) {
|
|
1574
|
+
var target = e.target;
|
|
1575
|
+
if (!target || !target.closest) return;
|
|
1576
|
+
var card = target.closest('.device-card[data-device-id]');
|
|
1577
|
+
if (!card) return;
|
|
1578
|
+
e.preventDefault();
|
|
1579
|
+
openDeviceDetail(card.getAttribute('data-device-id'));
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1420
1582
|
// --- Routing ---
|
|
1421
1583
|
|
|
1422
1584
|
window._currentFilterType = '';
|
|
@@ -1450,13 +1612,85 @@ mark{
|
|
|
1450
1612
|
return html;
|
|
1451
1613
|
}
|
|
1452
1614
|
|
|
1615
|
+
function findDeviceCard(deviceId) {
|
|
1616
|
+
var cards = document.querySelectorAll('.device-card[data-device-id]');
|
|
1617
|
+
for (var i = 0; i < cards.length; i += 1) {
|
|
1618
|
+
if (cards[i].getAttribute('data-device-id') === deviceId) return cards[i];
|
|
1619
|
+
}
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function updateVisibleIndexes(list) {
|
|
1624
|
+
Array.from(list.querySelectorAll('.log-entry')).forEach(function(entry, index) {
|
|
1625
|
+
entry.setAttribute('data-index', index);
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function visibleDeltaItems(deltaLogs) {
|
|
1630
|
+
var options = readVisibleLogOptions();
|
|
1631
|
+
var items = [];
|
|
1632
|
+
Object.entries(deltaLogs || {}).forEach(function(logGroup) {
|
|
1633
|
+
var type = logGroup[0];
|
|
1634
|
+
var entries = logGroup[1];
|
|
1635
|
+
if (!Array.isArray(entries)) return;
|
|
1636
|
+
entries.forEach(function(entry) {
|
|
1637
|
+
if (options.type && type !== options.type) return;
|
|
1638
|
+
if (options.failedOnly && !isFailedEntry(entry)) return;
|
|
1639
|
+
if (searchTerm && !entryMatchesSearch(entry)) return;
|
|
1640
|
+
items.push({ type: type, entry: entry, order: items.length });
|
|
1641
|
+
});
|
|
1642
|
+
});
|
|
1643
|
+
return items.sort(function(a, b) {
|
|
1644
|
+
var byTime = readTimestamp(b.entry) - readTimestamp(a.entry);
|
|
1645
|
+
return byTime || b.order - a.order;
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function appendDeltaLogs(deltaLogs) {
|
|
1650
|
+
var items = visibleDeltaItems(deltaLogs);
|
|
1651
|
+
updateCurrentPagination();
|
|
1652
|
+
if (!items.length) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (currentPage !== 1) {
|
|
1657
|
+
pendingLiveCount += items.length;
|
|
1658
|
+
updateLiveNotice();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
pendingLiveCount = 0;
|
|
1663
|
+
updateLiveNotice();
|
|
1664
|
+
|
|
1665
|
+
var container = document.getElementById('logsContainer');
|
|
1666
|
+
var list = container ? container.querySelector('.log-list') : null;
|
|
1667
|
+
if (!list) {
|
|
1668
|
+
rerenderVisibleLogs();
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
var html = '';
|
|
1673
|
+
items.forEach(function(item) {
|
|
1674
|
+
var rowId = getLogEntryKey(item.entry, item.type, 'live-' + (liveSequence += 1));
|
|
1675
|
+
html += renderLogEntryHtml(item.entry, item.type, rowId, 0, false);
|
|
1676
|
+
});
|
|
1677
|
+
list.insertAdjacentHTML('afterbegin', html);
|
|
1678
|
+
|
|
1679
|
+
while (list.querySelectorAll('.log-entry').length > PAGE_SIZE) {
|
|
1680
|
+
var last = list.lastElementChild;
|
|
1681
|
+
if (!last || last.classList.contains('expanded')) break;
|
|
1682
|
+
list.removeChild(last);
|
|
1683
|
+
}
|
|
1684
|
+
updateVisibleIndexes(list);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1453
1687
|
function appendDeviceCard(payload) {
|
|
1454
1688
|
var grid = document.querySelector('.device-grid');
|
|
1455
1689
|
if (!grid) { renderList(); return; }
|
|
1456
1690
|
|
|
1457
1691
|
var deviceId = payload.deviceId;
|
|
1458
1692
|
if (!deviceId) return;
|
|
1459
|
-
var existing =
|
|
1693
|
+
var existing = findDeviceCard(deviceId);
|
|
1460
1694
|
if (existing) {
|
|
1461
1695
|
var tags = existing.querySelector('.device-tags');
|
|
1462
1696
|
if (tags) tags.innerHTML = renderDeviceTags(payload.logCount || {});
|
|
@@ -1469,7 +1703,6 @@ mark{
|
|
|
1469
1703
|
var card = document.createElement('div');
|
|
1470
1704
|
card.className = 'device-card';
|
|
1471
1705
|
card.setAttribute('data-device-id', deviceId);
|
|
1472
|
-
card.setAttribute('onclick', "location.hash='device/" + encodeURIComponent(deviceId) + "'");
|
|
1473
1706
|
var html = '<div><div class="device-title">' + escapeHtml(deviceText) + '</div>';
|
|
1474
1707
|
html += '<div class="device-subtitle">IP ' + escapeHtml(ipText) + '</div></div>';
|
|
1475
1708
|
html += '<div class="device-meta-group">';
|
|
@@ -1488,83 +1721,6 @@ mark{
|
|
|
1488
1721
|
}
|
|
1489
1722
|
}
|
|
1490
1723
|
|
|
1491
|
-
function buildLogEntryHtml(entry, rowId, type, index) {
|
|
1492
|
-
var lt = type || getLogType(entry);
|
|
1493
|
-
var typeClass = toKeyPart(lt);
|
|
1494
|
-
var ts = readTimestamp(entry);
|
|
1495
|
-
var html = '<div class="log-row">';
|
|
1496
|
-
html += '<div class="log-type log-type-' + typeClass + '">' + escapeHtml(labelForType(lt)) + '</div>';
|
|
1497
|
-
html += '<div class="log-summary-col">';
|
|
1498
|
-
html += '<div class="log-summary">' + matchSearch(summarize(entry)) + '</div>';
|
|
1499
|
-
if (ts) {
|
|
1500
|
-
html += '<div class="log-timestamp">' + formatTimeShort(new Date(ts).toISOString()) + '</div>';
|
|
1501
|
-
}
|
|
1502
|
-
html += '</div>';
|
|
1503
|
-
html += '<div class="log-status">' + statusBadge(entry) + '</div>';
|
|
1504
|
-
html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">⎘</button></div>';
|
|
1505
|
-
html += '<div class="log-expand">▶</div>';
|
|
1506
|
-
html += '</div>';
|
|
1507
|
-
html += '<div class="log-detail" id="detail-' + rowId + '">';
|
|
1508
|
-
html += '<div class="log-detail-inner"><div class="detail-sections">';
|
|
1509
|
-
html += renderLogDetails(entry, lt);
|
|
1510
|
-
html += '<div class="entry-footer">';
|
|
1511
|
-
html += '<button class="btn btn-sm" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')">⎘ Copy JSON</button>';
|
|
1512
|
-
html += '</div>';
|
|
1513
|
-
html += '</div></div></div>';
|
|
1514
|
-
return html;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function appendDeltaLogs(deltaLogs) {
|
|
1518
|
-
var container = document.getElementById('logsContainer');
|
|
1519
|
-
if (!container) return;
|
|
1520
|
-
|
|
1521
|
-
var list = container.querySelector('.log-list');
|
|
1522
|
-
if (!list) {
|
|
1523
|
-
applyFilters();
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
var type = window._currentFilterType || '';
|
|
1528
|
-
var failedOnly = window._failedOnly || false;
|
|
1529
|
-
var limit = document.getElementById('limitInput') ? parseInt(document.getElementById('limitInput').value, 10) || 50 : 50;
|
|
1530
|
-
var allNewEntries = [];
|
|
1531
|
-
|
|
1532
|
-
Object.entries(deltaLogs).forEach(function(entry) {
|
|
1533
|
-
var t = entry[0];
|
|
1534
|
-
var entries = entry[1];
|
|
1535
|
-
if (!Array.isArray(entries)) return;
|
|
1536
|
-
entries.forEach(function(e) {
|
|
1537
|
-
if (failedOnly && !isFailedEntry(e)) return;
|
|
1538
|
-
if (type && t !== type) return;
|
|
1539
|
-
if (searchTerm && !entryMatchesSearch(e)) return;
|
|
1540
|
-
allNewEntries.push({ type: t, entry: e });
|
|
1541
|
-
});
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
var count = list.querySelectorAll('.log-entry').length;
|
|
1545
|
-
allNewEntries.forEach(function(item) {
|
|
1546
|
-
var entry = item.entry;
|
|
1547
|
-
var rowId = getLogEntryKey(entry, item.type, count++);
|
|
1548
|
-
var div = document.createElement('div');
|
|
1549
|
-
div.className = 'log-entry';
|
|
1550
|
-
div.id = 'entry-' + rowId;
|
|
1551
|
-
div.setAttribute('data-index', count - 1);
|
|
1552
|
-
div.setAttribute('onclick', '');
|
|
1553
|
-
div.querySelector('.log-row').setAttribute('onclick', "toggleRow('" + rowId + "')");
|
|
1554
|
-
div.innerHTML = buildLogEntryHtml(entry, rowId, item.type, count - 1);
|
|
1555
|
-
list.prepend(div);
|
|
1556
|
-
});
|
|
1557
|
-
|
|
1558
|
-
// Trim oldest rows from bottom if over limit, skip expanded entries.
|
|
1559
|
-
var entries = list.querySelectorAll('.log-entry');
|
|
1560
|
-
while (entries.length > limit) {
|
|
1561
|
-
var last = entries[entries.length - 1];
|
|
1562
|
-
if (last && last.classList.contains('expanded')) break;
|
|
1563
|
-
list.removeChild(last);
|
|
1564
|
-
entries = list.querySelectorAll('.log-entry');
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
1724
|
function updateTabCounts() {
|
|
1569
1725
|
if (!currentDevice) return;
|
|
1570
1726
|
var logs = currentDevice.report ? currentDevice.report.logs : {};
|