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.
Files changed (86) hide show
  1. package/README.md +4 -2
  2. package/README.zh-CN.md +4 -2
  3. package/lib/commonjs/core/DebugToolkit.js +118 -97
  4. package/lib/commonjs/core/DebugToolkit.js.map +1 -1
  5. package/lib/commonjs/core/initialize.js +4 -4
  6. package/lib/commonjs/core/initialize.js.map +1 -1
  7. package/lib/commonjs/features/environment/index.js +22 -24
  8. package/lib/commonjs/features/environment/index.js.map +1 -1
  9. package/lib/commonjs/features/network/index.js +25 -47
  10. package/lib/commonjs/features/network/index.js.map +1 -1
  11. package/lib/commonjs/features/network/networkInterceptor.js +3 -3
  12. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  13. package/lib/commonjs/index.js +0 -30
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/utils/DaemonClient.js +13 -51
  16. package/lib/commonjs/utils/DaemonClient.js.map +1 -1
  17. package/lib/commonjs/utils/createChannelFeature.js +8 -1
  18. package/lib/commonjs/utils/createChannelFeature.js.map +1 -1
  19. package/lib/commonjs/utils/deviceReport.js +2 -1
  20. package/lib/commonjs/utils/deviceReport.js.map +1 -1
  21. package/lib/commonjs/utils/urlRewriter.js +15 -0
  22. package/lib/commonjs/utils/urlRewriter.js.map +1 -0
  23. package/lib/module/core/DebugToolkit.js +117 -96
  24. package/lib/module/core/DebugToolkit.js.map +1 -1
  25. package/lib/module/core/initialize.js +6 -7
  26. package/lib/module/core/initialize.js.map +1 -1
  27. package/lib/module/features/environment/index.js +22 -24
  28. package/lib/module/features/environment/index.js.map +1 -1
  29. package/lib/module/features/network/index.js +25 -46
  30. package/lib/module/features/network/index.js.map +1 -1
  31. package/lib/module/features/network/networkInterceptor.js +3 -3
  32. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  33. package/lib/module/index.js +1 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/utils/DaemonClient.js +14 -42
  36. package/lib/module/utils/DaemonClient.js.map +1 -1
  37. package/lib/module/utils/createChannelFeature.js +8 -1
  38. package/lib/module/utils/createChannelFeature.js.map +1 -1
  39. package/lib/module/utils/deviceReport.js +3 -2
  40. package/lib/module/utils/deviceReport.js.map +1 -1
  41. package/lib/module/utils/urlRewriter.js +10 -0
  42. package/lib/module/utils/urlRewriter.js.map +1 -0
  43. package/lib/typescript/src/core/DebugToolkit.d.ts +23 -10
  44. package/lib/typescript/src/core/DebugToolkit.d.ts.map +1 -1
  45. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  46. package/lib/typescript/src/features/environment/index.d.ts.map +1 -1
  47. package/lib/typescript/src/features/network/index.d.ts +3 -3
  48. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  49. package/lib/typescript/src/index.d.ts +2 -2
  50. package/lib/typescript/src/index.d.ts.map +1 -1
  51. package/lib/typescript/src/types/feature.d.ts +5 -0
  52. package/lib/typescript/src/types/feature.d.ts.map +1 -1
  53. package/lib/typescript/src/types/index.d.ts +1 -1
  54. package/lib/typescript/src/types/index.d.ts.map +1 -1
  55. package/lib/typescript/src/utils/DaemonClient.d.ts +4 -11
  56. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
  57. package/lib/typescript/src/utils/createChannelFeature.d.ts +4 -0
  58. package/lib/typescript/src/utils/createChannelFeature.d.ts.map +1 -1
  59. package/lib/typescript/src/utils/deviceReport.d.ts +4 -1
  60. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
  61. package/lib/typescript/src/utils/urlRewriter.d.ts +5 -0
  62. package/lib/typescript/src/utils/urlRewriter.d.ts.map +1 -0
  63. package/node/daemon/src/console/console.html +324 -168
  64. package/node/daemon/src/server.js +32 -2
  65. package/node/mcp/src/logs.js +15 -4
  66. package/node/mcp/src/tools.js +4 -2
  67. package/package.json +1 -1
  68. package/src/core/DebugToolkit.tsx +119 -105
  69. package/src/core/initialize.ts +7 -8
  70. package/src/features/environment/index.ts +25 -27
  71. package/src/features/network/index.ts +30 -52
  72. package/src/features/network/networkInterceptor.ts +3 -3
  73. package/src/index.ts +3 -8
  74. package/src/types/feature.ts +6 -0
  75. package/src/types/index.ts +1 -0
  76. package/src/utils/DaemonClient.ts +14 -56
  77. package/src/utils/createChannelFeature.ts +12 -1
  78. package/src/utils/deviceReport.ts +5 -3
  79. package/src/utils/urlRewriter.ts +11 -0
  80. package/lib/commonjs/utils/urlRewriterRegistry.js +0 -14
  81. package/lib/commonjs/utils/urlRewriterRegistry.js.map +0 -1
  82. package/lib/module/utils/urlRewriterRegistry.js +0 -10
  83. package/lib/module/utils/urlRewriterRegistry.js.map +0 -1
  84. package/lib/typescript/src/utils/urlRewriterRegistry.d.ts +0 -7
  85. package/lib/typescript/src/utils/urlRewriterRegistry.d.ts.map +0 -1
  86. 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 + '">&#9654;</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 + '>&#9112;</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
- var hero = '<div class="network-hero"><span class="method-badge ' + methodClass + '">' + escapeHtml(method) + '</span>' +
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
- var reqRows = [
824
- ['Time', entry.timestamp ? formatTime(new Date(entry.timestamp).toISOString()) : ''],
825
- ['Duration', entry.duration !== undefined ? entry.duration + 'ms' : ''],
826
- ];
827
- if (request.headers) reqRows.push(['Headers', request.headers]);
828
- if (request.body) reqRows.push(['Body', request.body]);
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
- var html = renderSection('Request', hero + renderRows(reqRows), request);
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 resRows = [
834
- ['Status', response.status !== undefined ? response.status + (response.statusText ? ' ' + response.statusText : '') : ''],
835
- ['Success', response.success !== undefined ? String(response.success) : ''],
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" onclick="location.hash=\'device/' + encodeURIComponent(deviceLog.deviceId) + '\'">';
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" onclick="location.hash=\'\';return false">';
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=100'),
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=100'),
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()">&times;</button>';
1061
1159
  html += '</div>';
1062
1160
  html += '<label>Failed only <div class="toggle" id="failedToggle" onclick="toggleFailed()"></div></label>';
1063
- html += '<label>Limit <input type="number" id="limitInput" value="50" min="1" max="500"></label>';
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
- document.getElementById('limitInput').addEventListener('change', applyFilters);
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 renderLogs(logs, type, limit, failedOnly) {
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
- .slice(0, limit);
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">&#9112;</button></div>';
1263
+ html += '<div class="log-expand">' + (isExpanded ? '&#9654;' : '&#9654;') + '</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 + '\')">&#9112; 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 entry = item.entry;
1144
- var rowId = getLogEntryKey(entry, item.type, i);
1145
- var lt = item.type || getLogType(entry);
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">&#9112;</button></div>';
1161
- html += '<div class="log-expand">' + (isExpanded ? '&#9654;' : '&#9654;') + '</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 + '\')">&#9112; 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
- limit: document.getElementById('limitInput') ? parseInt(document.getElementById('limitInput').value, 10) || 50 : 50,
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.limit, options.failedOnly);
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
- if (options.type) {
1312
- entries = entries.filter(function(item) { return item.type === options.type; });
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 = grid.querySelector('[data-device-id="' + CSS.escape(deviceId) + '"]');
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">&#9112;</button></div>';
1505
- html += '<div class="log-expand">&#9654;</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 + '\')">&#9112; 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 : {};