react-native-debug-toolkit 2.3.0 → 3.0.0

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 (113) hide show
  1. package/bin/debug-toolkit.js +114 -0
  2. package/lib/commonjs/features/network/index.js +28 -2
  3. package/lib/commonjs/features/network/index.js.map +1 -1
  4. package/lib/commonjs/features/network/networkInterceptor.js +14 -6
  5. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  6. package/lib/commonjs/index.js +59 -0
  7. package/lib/commonjs/index.js.map +1 -1
  8. package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
  9. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  10. package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
  11. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  12. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +529 -0
  13. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
  14. package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
  15. package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
  16. package/lib/commonjs/utils/autoDetectDaemon.js +141 -0
  17. package/lib/commonjs/utils/autoDetectDaemon.js.map +1 -0
  18. package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
  19. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  20. package/lib/commonjs/utils/daemonConnection.js +81 -0
  21. package/lib/commonjs/utils/daemonConnection.js.map +1 -0
  22. package/lib/commonjs/utils/daemonSettings.js +110 -0
  23. package/lib/commonjs/utils/daemonSettings.js.map +1 -0
  24. package/lib/commonjs/utils/reportToDaemon.js +112 -0
  25. package/lib/commonjs/utils/reportToDaemon.js.map +1 -0
  26. package/lib/commonjs/utils/sessionReport.js +132 -0
  27. package/lib/commonjs/utils/sessionReport.js.map +1 -0
  28. package/lib/commonjs/utils/streamToDaemon.js +334 -0
  29. package/lib/commonjs/utils/streamToDaemon.js.map +1 -0
  30. package/lib/module/features/network/index.js +25 -1
  31. package/lib/module/features/network/index.js.map +1 -1
  32. package/lib/module/features/network/networkInterceptor.js +14 -6
  33. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  34. package/lib/module/index.js +5 -0
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/ui/panel/DebugPanel.js +26 -1
  37. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  38. package/lib/module/ui/panel/FloatPanelView.js +16 -63
  39. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  40. package/lib/module/ui/panel/StreamingSettingsModal.js +524 -0
  41. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
  42. package/lib/module/ui/panel/useTabAnimation.js +67 -0
  43. package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
  44. package/lib/module/utils/autoDetectDaemon.js +136 -0
  45. package/lib/module/utils/autoDetectDaemon.js.map +1 -0
  46. package/lib/module/utils/createPersistedObservableStore.js +23 -3
  47. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  48. package/lib/module/utils/daemonConnection.js +77 -0
  49. package/lib/module/utils/daemonConnection.js.map +1 -0
  50. package/lib/module/utils/daemonSettings.js +102 -0
  51. package/lib/module/utils/daemonSettings.js.map +1 -0
  52. package/lib/module/utils/reportToDaemon.js +105 -0
  53. package/lib/module/utils/reportToDaemon.js.map +1 -0
  54. package/lib/module/utils/sessionReport.js +128 -0
  55. package/lib/module/utils/sessionReport.js.map +1 -0
  56. package/lib/module/utils/streamToDaemon.js +328 -0
  57. package/lib/module/utils/streamToDaemon.js.map +1 -0
  58. package/lib/typescript/src/features/network/index.d.ts +2 -0
  59. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  60. package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
  61. package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
  62. package/lib/typescript/src/index.d.ts +10 -0
  63. package/lib/typescript/src/index.d.ts.map +1 -1
  64. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  65. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  66. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
  67. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
  68. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
  69. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
  70. package/lib/typescript/src/utils/autoDetectDaemon.d.ts +15 -0
  71. package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +1 -0
  72. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
  73. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  74. package/lib/typescript/src/utils/daemonConnection.d.ts +18 -0
  75. package/lib/typescript/src/utils/daemonConnection.d.ts.map +1 -0
  76. package/lib/typescript/src/utils/daemonSettings.d.ts +19 -0
  77. package/lib/typescript/src/utils/daemonSettings.d.ts.map +1 -0
  78. package/lib/typescript/src/utils/reportToDaemon.d.ts +34 -0
  79. package/lib/typescript/src/utils/reportToDaemon.d.ts.map +1 -0
  80. package/lib/typescript/src/utils/sessionReport.d.ts +18 -0
  81. package/lib/typescript/src/utils/sessionReport.d.ts.map +1 -0
  82. package/lib/typescript/src/utils/streamToDaemon.d.ts +23 -0
  83. package/lib/typescript/src/utils/streamToDaemon.d.ts.map +1 -0
  84. package/node/daemon/src/cli.js +75 -0
  85. package/node/daemon/src/console/console.html +936 -0
  86. package/node/daemon/src/console/index.js +47 -0
  87. package/node/daemon/src/constants.js +32 -0
  88. package/node/daemon/src/index.js +11 -0
  89. package/node/daemon/src/server.js +365 -0
  90. package/node/daemon/src/store.js +110 -0
  91. package/node/mcp/src/cli.js +31 -0
  92. package/node/mcp/src/constants.js +13 -0
  93. package/node/mcp/src/daemonClient.js +132 -0
  94. package/node/mcp/src/httpClient.js +49 -0
  95. package/node/mcp/src/index.js +15 -0
  96. package/node/mcp/src/logs.js +95 -0
  97. package/node/mcp/src/server.js +144 -0
  98. package/node/mcp/src/tools.js +84 -0
  99. package/package.json +7 -2
  100. package/src/features/network/index.ts +30 -3
  101. package/src/features/network/networkInterceptor.ts +19 -6
  102. package/src/index.ts +14 -0
  103. package/src/ui/panel/DebugPanel.tsx +23 -1
  104. package/src/ui/panel/FloatPanelView.tsx +10 -68
  105. package/src/ui/panel/StreamingSettingsModal.tsx +566 -0
  106. package/src/ui/panel/useTabAnimation.ts +77 -0
  107. package/src/utils/autoDetectDaemon.ts +175 -0
  108. package/src/utils/createPersistedObservableStore.ts +16 -3
  109. package/src/utils/daemonConnection.ts +133 -0
  110. package/src/utils/daemonSettings.ts +134 -0
  111. package/src/utils/reportToDaemon.ts +172 -0
  112. package/src/utils/sessionReport.ts +203 -0
  113. package/src/utils/streamToDaemon.ts +419 -0
@@ -0,0 +1,936 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Debug Toolkit Console</title>
7
+ <style>
8
+ *{margin:0;padding:0;box-sizing:border-box}
9
+ :root{
10
+ --bg:#080c16;--bg2:#0d1220;--surface:#111827;--surface2:#1a2236;--surface3:#222d44;
11
+ --border:#1e2d4a;--border2:#2a3f66;
12
+ --text:#e2e8f0;--text2:#8899b4;--text3:#5a6e8a;
13
+ --cyan:#00e5ff;--cyan-dim:rgba(0,229,255,.12);--cyan-mid:rgba(0,229,255,.25);
14
+ --green:#00e676;--green-dim:rgba(0,230,118,.12);
15
+ --red:#ff1744;--red-dim:rgba(255,23,68,.12);
16
+ --amber:#ffab00;--amber-dim:rgba(255,171,0,.12);
17
+ --orange:#ff6e40;--orange-dim:rgba(255,110,64,.12);
18
+ --font-mono:'SF Mono',Monaco,Consolas,monospace;
19
+ --font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
20
+ --radius:6px;
21
+ }
22
+
23
+ html{scrollbar-width:thin;scrollbar-color:var(--border2) transparent}
24
+ ::-webkit-scrollbar{width:6px;height:6px}
25
+ ::-webkit-scrollbar-track{background:transparent}
26
+ ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
27
+
28
+ body{
29
+ font-family:var(--font-sans);background:var(--bg);color:var(--text);
30
+ line-height:1.6;min-height:100vh;
31
+ background-image:
32
+ repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,229,255,.012) 2px,rgba(0,229,255,.012) 4px);
33
+ }
34
+
35
+ /* Scanline overlay */
36
+ body::after{
37
+ content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;
38
+ background:repeating-linear-gradient(0deg,transparent 0px,transparent 1px,rgba(0,0,0,.03) 1px,rgba(0,0,0,.03) 2px);
39
+ }
40
+
41
+ a{color:var(--cyan);text-decoration:none;transition:color .15s}
42
+ a:hover{color:#fff}
43
+
44
+ /* Header */
45
+ header{
46
+ display:flex;align-items:center;justify-content:space-between;
47
+ padding:0 24px;height:56px;
48
+ background:linear-gradient(180deg,var(--bg2) 0%,rgba(13,18,32,.95) 100%);
49
+ border-bottom:1px solid var(--border);
50
+ position:sticky;top:0;z-index:100;
51
+ backdrop-filter:blur(12px);
52
+ }
53
+ .header-left{display:flex;align-items:center;gap:14px}
54
+ .pulse{
55
+ width:8px;height:8px;border-radius:50%;background:var(--cyan);flex-shrink:0;
56
+ box-shadow:0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3);
57
+ animation:pulse 2s ease-in-out infinite;
58
+ }
59
+ @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}}
60
+ header h1{font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--text)}
61
+ header h1 span{color:var(--text3);font-weight:400}
62
+ .header-right{display:flex;align-items:center;gap:10px}
63
+ .header-meta{font-size:11px;font-family:var(--font-mono);color:var(--text3)}
64
+
65
+ /* Buttons */
66
+ .btn{
67
+ display:inline-flex;align-items:center;gap:6px;
68
+ padding:6px 14px;font-size:12px;font-weight:500;
69
+ border:1px solid var(--border2);border-radius:var(--radius);
70
+ background:var(--surface2);color:var(--text2);cursor:pointer;
71
+ font-family:var(--font-sans);transition:all .15s;letter-spacing:.01em;
72
+ }
73
+ .btn:hover{background:var(--surface3);color:var(--text);border-color:var(--cyan-mid)}
74
+ .btn:active{transform:scale(.97)}
75
+ .btn-icon{padding:6px 10px;font-size:14px}
76
+
77
+ /* Container */
78
+ .container{max-width:1160px;margin:0 auto;padding:28px 24px}
79
+
80
+ /* Empty state */
81
+ .empty{text-align:center;padding:80px 20px;color:var(--text3)}
82
+ .empty-icon{font-size:40px;margin-bottom:16px;opacity:.3;font-family:var(--font-mono)}
83
+ .empty p{font-size:14px;line-height:1.8}
84
+ .empty code{
85
+ font-family:var(--font-mono);font-size:12px;
86
+ background:var(--surface);padding:2px 8px;border-radius:3px;
87
+ border:1px solid var(--border);color:var(--cyan);
88
+ }
89
+
90
+ /* Session list */
91
+ .section-title{
92
+ font-size:13px;font-weight:600;color:var(--text2);
93
+ text-transform:uppercase;letter-spacing:.08em;
94
+ margin-bottom:16px;display:flex;align-items:center;gap:8px;
95
+ }
96
+ .section-title::after{content:'';flex:1;height:1px;background:var(--border)}
97
+
98
+ .session-grid{display:flex;flex-direction:column;gap:6px}
99
+ .session-card{
100
+ display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:16px;
101
+ padding:14px 18px;background:var(--surface);
102
+ border:1px solid var(--border);border-radius:var(--radius);
103
+ cursor:pointer;transition:all .15s;
104
+ }
105
+ .session-card:hover{
106
+ background:var(--surface2);border-color:var(--border2);
107
+ transform:translateX(3px);
108
+ box-shadow:0 0 0 1px var(--cyan-dim),0 4px 16px rgba(0,0,0,.3);
109
+ }
110
+ .session-id{font-family:var(--font-mono);font-size:12px;color:var(--text);font-weight:500}
111
+ .session-time{font-size:11px;color:var(--text3);font-family:var(--font-mono);margin-top:2px}
112
+ .session-tags{display:flex;gap:6px;flex-wrap:wrap}
113
+ .tag{
114
+ font-family:var(--font-mono);font-size:10px;font-weight:500;
115
+ padding:2px 8px;border-radius:3px;
116
+ background:var(--cyan-dim);color:var(--cyan);
117
+ border:1px solid rgba(0,229,255,.1);
118
+ }
119
+ .tag-network{background:var(--cyan-dim);color:var(--cyan);border-color:rgba(0,229,255,.1)}
120
+ .tag-console{background:var(--green-dim);color:var(--green);border-color:rgba(0,230,118,.1)}
121
+ .tag-error{background:var(--red-dim);color:var(--red);border-color:rgba(255,23,68,.1)}
122
+ .tag-warn{background:var(--amber-dim);color:var(--amber);border-color:rgba(255,171,0,.1)}
123
+ .tag-track{background:var(--orange-dim);color:var(--orange);border-color:rgba(255,110,64,.1)}
124
+ .session-arrow{color:var(--text3);font-size:16px;transition:color .15s}
125
+ .session-card:hover .session-arrow{color:var(--cyan)}
126
+
127
+ /* Detail view */
128
+ .back-link{
129
+ display:inline-flex;align-items:center;gap:6px;
130
+ font-size:12px;color:var(--text3);margin-bottom:20px;
131
+ padding:6px 0;transition:color .15s;
132
+ }
133
+ .back-link:hover{color:var(--cyan)}
134
+ .back-link svg{width:14px;height:14px}
135
+
136
+ .detail-header{
137
+ background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
138
+ padding:20px 24px;margin-bottom:20px;
139
+ border-left:3px solid var(--cyan);
140
+ }
141
+ .detail-id{font-family:var(--font-mono);font-size:13px;color:var(--cyan);font-weight:500}
142
+ .detail-meta{
143
+ display:flex;gap:20px;margin-top:10px;flex-wrap:wrap;
144
+ }
145
+ .detail-meta-item{font-size:11px;color:var(--text3);font-family:var(--font-mono)}
146
+ .detail-meta-item strong{color:var(--text2);font-weight:500}
147
+
148
+ /* Tabs */
149
+ .tabs{display:flex;gap:2px;margin-bottom:18px;padding:3px;background:var(--bg2);border-radius:var(--radius);border:1px solid var(--border);overflow-x:auto}
150
+ .tab{
151
+ padding:7px 16px;font-size:12px;font-weight:500;
152
+ border:none;border-radius:4px;background:transparent;
153
+ color:var(--text3);cursor:pointer;font-family:var(--font-sans);
154
+ transition:all .15s;white-space:nowrap;letter-spacing:.01em;
155
+ }
156
+ .tab:hover{color:var(--text2);background:var(--surface)}
157
+ .tab.active{
158
+ background:var(--cyan);color:var(--bg);font-weight:600;
159
+ box-shadow:0 0 12px rgba(0,229,255,.25);
160
+ }
161
+ .tab .count{font-weight:400;opacity:.7;margin-left:3px;font-size:11px}
162
+
163
+ /* Toolbar */
164
+ .toolbar{
165
+ display:flex;align-items:center;gap:14px;margin-bottom:18px;
166
+ padding:10px 16px;background:var(--bg2);border-radius:var(--radius);
167
+ border:1px solid var(--border);flex-wrap:wrap;
168
+ }
169
+ .toolbar label{
170
+ font-size:12px;color:var(--text2);display:flex;align-items:center;gap:6px;
171
+ font-family:var(--font-mono);cursor:pointer;
172
+ }
173
+ .toolbar input[type=number]{
174
+ width:56px;padding:4px 8px;
175
+ background:var(--surface);border:1px solid var(--border2);border-radius:4px;
176
+ color:var(--text);font-size:11px;font-family:var(--font-mono);
177
+ }
178
+ .toolbar input[type=number]:focus{outline:none;border-color:var(--cyan)}
179
+ .toggle{
180
+ position:relative;width:32px;height:18px;
181
+ background:var(--surface3);border-radius:9px;cursor:pointer;
182
+ transition:background .15s;border:1px solid var(--border2);
183
+ }
184
+ .toggle.on{background:var(--red);border-color:var(--red)}
185
+ .toggle::after{
186
+ content:'';position:absolute;top:2px;left:2px;
187
+ width:12px;height:12px;border-radius:50%;background:#fff;
188
+ transition:transform .15s;
189
+ }
190
+ .toggle.on::after{transform:translateX(14px)}
191
+
192
+ /* Log entries */
193
+ .log-list{display:flex;flex-direction:column;gap:2px}
194
+ .log-entry{
195
+ display:grid;grid-template-columns:80px 1fr 90px 30px;
196
+ align-items:center;gap:0;
197
+ background:var(--surface);border:1px solid transparent;border-radius:var(--radius);
198
+ cursor:pointer;transition:all .15s;overflow:hidden;
199
+ animation:fadeSlideIn .3s ease-out both;
200
+ }
201
+ @keyframes fadeSlideIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
202
+ .log-entry:hover{
203
+ background:var(--surface2);border-color:var(--border2);
204
+ box-shadow:0 2px 8px rgba(0,0,0,.2);
205
+ }
206
+ .log-entry.expanded{border-color:var(--cyan-mid);background:var(--surface2)}
207
+ .log-type{
208
+ font-family:var(--font-mono);font-size:10px;font-weight:600;
209
+ text-transform:uppercase;letter-spacing:.06em;
210
+ padding:0 14px;height:42px;display:flex;align-items:center;
211
+ border-right:1px solid var(--border);
212
+ }
213
+ .log-type-network{color:var(--cyan)}
214
+ .log-type-console{color:var(--green)}
215
+ .log-type-navigation{color:#7c4dff}
216
+ .log-type-track{color:var(--orange)}
217
+ .log-type-zustand{color:#e040fb}
218
+ .log-type-unknown{color:var(--text3)}
219
+ .log-summary{
220
+ padding:0 14px;height:42px;display:flex;align-items:center;
221
+ font-family:var(--font-mono);font-size:12px;color:var(--text);
222
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
223
+ }
224
+ .log-status{
225
+ padding:0 10px;height:42px;display:flex;align-items:center;justify-content:center;
226
+ }
227
+ .badge{
228
+ font-family:var(--font-mono);font-size:10px;font-weight:600;
229
+ padding:2px 8px;border-radius:3px;letter-spacing:.02em;
230
+ }
231
+ .badge-ok{background:var(--green-dim);color:var(--green)}
232
+ .badge-error{background:var(--red-dim);color:var(--red)}
233
+ .badge-warn{background:var(--amber-dim);color:var(--amber)}
234
+ .badge-info{background:var(--cyan-dim);color:var(--cyan)}
235
+ .log-expand{
236
+ padding:0 8px;height:42px;display:flex;align-items:center;justify-content:center;
237
+ color:var(--text3);font-size:10px;transition:color .15s;
238
+ }
239
+ .log-entry:hover .log-expand{color:var(--cyan)}
240
+ .log-json{
241
+ grid-column:1/-1;
242
+ border-top:1px solid var(--border);
243
+ overflow:hidden;
244
+ max-height:0;transition:max-height .3s ease-out;
245
+ }
246
+ .log-json.open{max-height:400px}
247
+ .log-json pre{
248
+ padding:14px 18px;font-family:var(--font-mono);font-size:11px;
249
+ line-height:1.6;color:var(--text2);white-space:pre-wrap;word-break:break-all;
250
+ overflow:auto;max-height:400px;
251
+ }
252
+
253
+ /* Actions bar */
254
+ .actions{
255
+ display:flex;gap:8px;margin-top:20px;padding-top:16px;
256
+ border-top:1px solid var(--border);
257
+ }
258
+
259
+ /* Toast */
260
+ .toast{
261
+ position:fixed;bottom:24px;right:24px;
262
+ padding:10px 20px;
263
+ background:var(--cyan);color:var(--bg);
264
+ border-radius:var(--radius);font-size:13px;font-weight:600;
265
+ font-family:var(--font-sans);
266
+ box-shadow:0 4px 20px rgba(0,229,255,.3);
267
+ transform:translateY(80px);opacity:0;transition:all .3s cubic-bezier(.2,.9,.3,1);
268
+ z-index:1000;
269
+ }
270
+ .toast.show{transform:translateY(0);opacity:1}
271
+
272
+ /* Stagger animations */
273
+ .log-entry:nth-child(1){animation-delay:.0s}
274
+ .log-entry:nth-child(2){animation-delay:.02s}
275
+ .log-entry:nth-child(3){animation-delay:.04s}
276
+ .log-entry:nth-child(4){animation-delay:.06s}
277
+ .log-entry:nth-child(5){animation-delay:.08s}
278
+ .log-entry:nth-child(n+6){animation-delay:.1s}
279
+
280
+ /* Device info */
281
+ .device-info{
282
+ display:flex;gap:16px;align-items:center;flex-wrap:wrap;
283
+ margin-top:10px;padding-top:10px;border-top:1px solid var(--border);
284
+ }
285
+ .device-badge{
286
+ display:inline-flex;align-items:center;gap:4px;
287
+ font-family:var(--font-mono);font-size:11px;
288
+ padding:3px 10px;border-radius:4px;
289
+ background:var(--surface2);color:var(--text2);
290
+ border:1px solid var(--border);
291
+ }
292
+ .device-badge.platform-ios{border-color:rgba(255,255,255,.2);color:#fff}
293
+ .device-badge.platform-android{border-color:rgba(0,230,118,.2);color:var(--green)}
294
+ .live-badge{
295
+ display:inline-flex;align-items:center;gap:6px;
296
+ font-family:var(--font-mono);font-size:11px;
297
+ padding:3px 10px;border-radius:4px;
298
+ background:var(--cyan-dim);color:var(--cyan);
299
+ border:1px solid rgba(0,229,255,.15);
300
+ animation:livePulse 2s ease-in-out infinite;
301
+ }
302
+ @keyframes livePulse{0%,100%{opacity:1}50%{opacity:.6}}
303
+ .live-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
304
+
305
+ /* Responsive */
306
+ @media(max-width:640px){
307
+ .container{padding:16px}
308
+ .session-card{grid-template-columns:1fr;gap:8px}
309
+ .log-entry{grid-template-columns:60px 1fr 70px 24px}
310
+ .detail-header{padding:14px 16px}
311
+ .tabs{overflow-x:auto;-webkit-overflow-scrolling:touch}
312
+ }
313
+ </style>
314
+ </head>
315
+ <body>
316
+ <header>
317
+ <div class="header-left">
318
+ <div class="pulse" id="pulseDot"></div>
319
+ <h1>Debug Toolkit <span>Console</span></h1>
320
+ </div>
321
+ <div class="header-right">
322
+ <span class="header-meta" id="status"></span>
323
+ <button class="btn" onclick="refresh()">
324
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
325
+ Refresh
326
+ </button>
327
+ </div>
328
+ </header>
329
+ <div class="container" id="app"></div>
330
+ <div class="toast" id="toast"></div>
331
+
332
+ <script>
333
+ (function() {
334
+ 'use strict';
335
+
336
+ var app = document.getElementById('app');
337
+ var statusEl = document.getElementById('status');
338
+ var toastEl = document.getElementById('toast');
339
+ var pulseDot = document.getElementById('pulseDot');
340
+ var currentSession = null;
341
+ var expandedRows = {};
342
+ var authToken = null;
343
+
344
+ try {
345
+ var params = new URLSearchParams(location.search);
346
+ authToken = params.get('token') || sessionStorage.getItem('debugToolkitToken');
347
+ if (authToken) sessionStorage.setItem('debugToolkitToken', authToken);
348
+ } catch {
349
+ authToken = null;
350
+ }
351
+
352
+ function withAuth(path) {
353
+ if (!authToken) return path;
354
+ var join = path.indexOf('?') >= 0 ? '&' : '?';
355
+ return path + join + 'token=' + encodeURIComponent(authToken);
356
+ }
357
+
358
+ function api(path) {
359
+ return fetch(withAuth(path)).then(function(r) { return r.json(); });
360
+ }
361
+
362
+ function showToast(msg) {
363
+ toastEl.textContent = msg;
364
+ toastEl.classList.add('show');
365
+ setTimeout(function() { toastEl.classList.remove('show'); }, 2000);
366
+ }
367
+
368
+ function escapeHtml(s) {
369
+ var d = document.createElement('div');
370
+ d.textContent = s;
371
+ return d.innerHTML;
372
+ }
373
+
374
+ function formatTime(iso) {
375
+ if (!iso) return '-';
376
+ try {
377
+ var d = new Date(iso);
378
+ var pad = function(n) { return String(n).padStart(2, '0'); };
379
+ return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) +
380
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
381
+ } catch { return iso; }
382
+ }
383
+
384
+ function formatTimeShort(iso) {
385
+ if (!iso) return '';
386
+ try {
387
+ var d = new Date(iso);
388
+ return d.toLocaleTimeString();
389
+ } catch { return iso; }
390
+ }
391
+
392
+ function statusBadge(entry) {
393
+ if (!entry || typeof entry !== 'object') return '<span class="badge badge-info">-</span>';
394
+ if (entry.error) return '<span class="badge badge-error">ERR</span>';
395
+ if (entry.response) {
396
+ var s = entry.response.status;
397
+ if (s >= 500) return '<span class="badge badge-error">' + s + '</span>';
398
+ if (s >= 400) return '<span class="badge badge-warn">' + s + '</span>';
399
+ if (s >= 200 && s < 300) return '<span class="badge badge-ok">' + s + '</span>';
400
+ return '<span class="badge badge-info">' + s + '</span>';
401
+ }
402
+ if (entry.level === 'error') return '<span class="badge badge-error">ERR</span>';
403
+ if (entry.level === 'warn') return '<span class="badge badge-warn">WRN</span>';
404
+ return '<span class="badge badge-ok">OK</span>';
405
+ }
406
+
407
+ function summarize(entry) {
408
+ if (!entry || typeof entry !== 'object') return escapeHtml(String(entry));
409
+ if (entry.method && entry.url) return escapeHtml(entry.method + ' ' + entry.url);
410
+ if (entry.level && entry.data) return escapeHtml((Array.isArray(entry.data) ? entry.data.join(' ') : String(entry.data)).substring(0, 120));
411
+ if (entry.path) return escapeHtml(entry.path);
412
+ if (entry.event) return escapeHtml(entry.event);
413
+ if (entry.action) return escapeHtml(entry.action);
414
+ return escapeHtml(JSON.stringify(entry).substring(0, 120));
415
+ }
416
+
417
+ function getLogType(entry) {
418
+ if (!entry || typeof entry !== 'object') return 'unknown';
419
+ if (entry.method && entry.url) return 'network';
420
+ if (entry.level && entry.data !== undefined) return 'console';
421
+ if (entry.path) return 'navigation';
422
+ if (entry.event) return 'track';
423
+ if (entry.action) return 'zustand';
424
+ return 'unknown';
425
+ }
426
+
427
+ function toKeyPart(value) {
428
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80);
429
+ }
430
+
431
+ function stringifyForKey(value) {
432
+ try {
433
+ return JSON.stringify(value).slice(0, 80);
434
+ } catch {
435
+ return String(value);
436
+ }
437
+ }
438
+
439
+ function getLogEntryKey(entry, type, index) {
440
+ var logType = toKeyPart(type || getLogType(entry));
441
+ if (entry && typeof entry === 'object') {
442
+ if (entry.id !== undefined && entry.id !== null) {
443
+ return logType + '-id-' + toKeyPart(entry.id);
444
+ }
445
+ if (entry.timestamp !== undefined && entry.timestamp !== null) {
446
+ return logType + '-ts-' + toKeyPart(entry.timestamp) + '-' + index;
447
+ }
448
+ if (entry.request && typeof entry.request === 'object' && entry.request.url) {
449
+ return logType + '-req-' + toKeyPart((entry.request.method || '') + '-' + entry.request.url + '-' + index);
450
+ }
451
+ }
452
+ return logType + '-fallback-' + index + '-' + toKeyPart(stringifyForKey(entry));
453
+ }
454
+
455
+ // --- Views ---
456
+
457
+ function renderList() {
458
+ currentSession = null;
459
+ expandedRows = {};
460
+ pulseDot.style.background = 'var(--text3)';
461
+ pulseDot.style.boxShadow = 'none';
462
+ statusEl.textContent = 'fetching...';
463
+ api('/sessions').then(function(data) {
464
+ statusEl.textContent = '';
465
+ pulseDot.style.background = 'var(--cyan)';
466
+ pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
467
+ var sessions = data.sessions || [];
468
+ if (!sessions.length) {
469
+ app.innerHTML =
470
+ '<div class="empty">' +
471
+ '<div class="empty-icon">_</div>' +
472
+ '<p>No sessions received yet.</p>' +
473
+ '<p style="margin-top:12px;font-size:13px">POST a report to <code>/report</code> to see data here.</p>' +
474
+ '</div>';
475
+ return;
476
+ }
477
+ var html = '<div class="section-title">Sessions <span style="color:var(--text3);font-weight:400">(' + sessions.length + ')</span></div>';
478
+ html += '<div class="session-grid">';
479
+ sessions.forEach(function(s, i) {
480
+ var lc = s.logCount || {};
481
+ html += '<div class="session-card" data-sid="' + escapeHtml(s.sessionId) + '" style="animation-delay:' + (i * 40) + 'ms" onclick="location.hash=\'session/' + encodeURIComponent(s.sessionId) + '\'">';
482
+ html += '<div><div class="session-id">' + escapeHtml(s.sessionId) + '</div>';
483
+ html += '<div class="session-time">' + formatTime(s.receivedAt) + '</div></div>';
484
+ html += '<div class="session-tags">' + renderSessionTags(lc) + '</div>';
485
+ html += '<div class="session-arrow">&rsaquo;</div>';
486
+ html += '</div>';
487
+ });
488
+ html += '</div>';
489
+ app.innerHTML = html;
490
+ }).catch(function(err) {
491
+ statusEl.textContent = '';
492
+ pulseDot.style.background = 'var(--red)';
493
+ pulseDot.style.boxShadow = '0 0 8px var(--red)';
494
+ app.innerHTML = '<div class="empty" style="color:var(--red)">Connection failed: ' + escapeHtml(err.message) + '</div>';
495
+ });
496
+ }
497
+
498
+ function renderDetail(sessionId) {
499
+ expandedRows = {};
500
+ statusEl.textContent = 'loading...';
501
+ api('/sessions/' + encodeURIComponent(sessionId)).then(function(data) {
502
+ statusEl.textContent = '';
503
+ pulseDot.style.background = 'var(--cyan)';
504
+ pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
505
+ currentSession = data;
506
+ var report = data.report || {};
507
+ var logs = report.logs || {};
508
+ var logTypes = Object.keys(logs);
509
+
510
+ var html = '';
511
+
512
+ // Back link
513
+ html += '<a href="#" class="back-link" onclick="location.hash=\'\';return false">';
514
+ 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>';
515
+ html += 'All sessions</a>';
516
+
517
+ // Session header card
518
+ html += '<div class="detail-header">';
519
+ html += '<div class="detail-id">' + escapeHtml(data.sessionId) + '</div>';
520
+ html += '<div class="detail-meta">';
521
+ html += '<span class="detail-meta-item"><strong>Received</strong> ' + formatTime(data.receivedAt) + '</span>';
522
+ var totalLogs = Object.values(data.logCount || {}).reduce(function(a, b) { return a + b; }, 0);
523
+ html += '<span class="detail-meta-item" data-type="Entries"><strong>Entries</strong> ' + totalLogs + '</span>';
524
+ Object.entries(data.logCount || {}).forEach(function(e) {
525
+ html += '<span class="detail-meta-item" data-type="' + e[0] + '"><strong>' + e[0] + '</strong> ' + e[1] + '</span>';
526
+ });
527
+ html += '</div>';
528
+
529
+ // Device info
530
+ var device = report.device;
531
+ if (device && typeof device === 'object') {
532
+ html += '<div class="device-info">';
533
+ var pClass = device.platform === 'ios' ? 'platform-ios' : device.platform === 'android' ? 'platform-android' : '';
534
+ html += '<span class="device-badge ' + pClass + '">' + escapeHtml((device.platform || 'unknown').toUpperCase()) + '</span>';
535
+ if (device.model) html += '<span class="device-badge">' + escapeHtml(device.model) + '</span>';
536
+ if (device.osVersion) html += '<span class="device-badge">OS ' + escapeHtml(device.osVersion) + '</span>';
537
+ if (device.appVersion) html += '<span class="device-badge">v' + escapeHtml(device.appVersion) + '</span>';
538
+ if (sseConnected) html += '<span class="live-badge"><span class="live-badge-dot"></span>LIVE</span>';
539
+ html += '</div>';
540
+ } else if (sseConnected) {
541
+ html += '<div class="device-info"><span class="live-badge"><span class="live-badge-dot"></span>LIVE</span></div>';
542
+ }
543
+
544
+ html += '</div>';
545
+
546
+ // Tabs
547
+ html += '<div class="tabs">';
548
+ html += '<button class="tab active" data-type="" onclick="filterType(this,\'\')">All<span class="count">' + totalLogs + '</span></button>';
549
+ logTypes.forEach(function(t) {
550
+ var count = logs[t] ? logs[t].length : 0;
551
+ html += '<button class="tab" data-type="' + t + '" onclick="filterType(this,\'' + t + '\')">' + escapeHtml(t) + '<span class="count">' + count + '</span></button>';
552
+ });
553
+ html += '</div>';
554
+
555
+ // Toolbar
556
+ html += '<div class="toolbar">';
557
+ html += '<label>Failed only <div class="toggle" id="failedToggle" onclick="toggleFailed()"></div></label>';
558
+ html += '<label>Limit <input type="number" id="limitInput" value="50" min="1" max="500"></label>';
559
+ html += '</div>';
560
+
561
+ // Log container
562
+ html += '<div id="logsContainer"></div>';
563
+
564
+ // Actions
565
+ html += '<div class="actions">';
566
+ html += '<button class="btn" onclick="copyJSON()">';
567
+ html += '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
568
+ html += 'Copy JSON</button>';
569
+ html += '</div>';
570
+
571
+ app.innerHTML = html;
572
+ document.getElementById('limitInput').addEventListener('change', applyFilters);
573
+ renderLogs(logs, '', 50, false);
574
+ }).catch(function(err) {
575
+ statusEl.textContent = '';
576
+ app.innerHTML = '<div class="empty" style="color:var(--red)">Failed to load: ' + escapeHtml(err.message) + '</div>';
577
+ });
578
+ }
579
+
580
+ function renderLogs(logs, type, limit, failedOnly) {
581
+ var entries = [];
582
+ if (type && logs[type]) {
583
+ entries = Array.isArray(logs[type])
584
+ ? logs[type].map(function(entry) { return { type: type, entry: entry }; })
585
+ : [];
586
+ } else {
587
+ Object.entries(logs).forEach(function(logGroup) {
588
+ var logType = logGroup[0];
589
+ var value = logGroup[1];
590
+ if (Array.isArray(value)) {
591
+ value.forEach(function(entry) {
592
+ entries.push({ type: logType, entry: entry });
593
+ });
594
+ }
595
+ });
596
+ }
597
+
598
+ if (failedOnly) {
599
+ entries = entries.filter(function(item) { return isFailedEntry(item.entry); });
600
+ }
601
+
602
+ entries = entries.slice(-limit);
603
+
604
+ var container = document.getElementById('logsContainer');
605
+ if (!entries.length) {
606
+ container.innerHTML = '<div class="empty" style="padding:40px"><div class="empty-icon" style="font-size:24px">0</div><p style="font-size:13px">No logs match filters.</p></div>';
607
+ return;
608
+ }
609
+
610
+ var html = '<div class="log-list">';
611
+ entries.forEach(function(item, i) {
612
+ var entry = item.entry;
613
+ var rowId = getLogEntryKey(entry, item.type, i);
614
+ var lt = item.type || getLogType(entry);
615
+ var typeClass = toKeyPart(lt);
616
+ var isExpanded = expandedRows[rowId];
617
+ html += '<div class="log-entry' + (isExpanded ? ' expanded' : '') + '" id="entry-' + rowId + '" onclick="toggleRow(\'' + rowId + '\')">';
618
+ html += '<div class="log-type log-type-' + typeClass + '">' + escapeHtml(lt.substring(0,4)) + '</div>';
619
+ html += '<div class="log-summary">' + summarize(entry) + '</div>';
620
+ html += '<div class="log-status">' + statusBadge(entry) + '</div>';
621
+ html += '<div class="log-expand">' + (isExpanded ? '&#9660;' : '&#9654;') + '</div>';
622
+ html += '<div class="log-json' + (isExpanded ? ' open' : '') + '" id="json-' + rowId + '"><pre>' + escapeHtml(JSON.stringify(entry, null, 2)) + '</pre></div>';
623
+ html += '</div>';
624
+ });
625
+ html += '</div>';
626
+ container.innerHTML = html;
627
+ }
628
+
629
+ // --- Global handlers ---
630
+
631
+ function readVisibleLogOptions() {
632
+ return {
633
+ type: window._currentFilterType || '',
634
+ failedOnly: window._failedOnly || false,
635
+ limit: document.getElementById('limitInput') ? parseInt(document.getElementById('limitInput').value, 10) || 50 : 50,
636
+ };
637
+ }
638
+
639
+ function rerenderVisibleLogs() {
640
+ if (!currentSession) return;
641
+ var logs = currentSession.report ? currentSession.report.logs : {};
642
+ var options = readVisibleLogOptions();
643
+ renderLogs(logs, options.type, options.limit, options.failedOnly);
644
+ }
645
+
646
+ function refreshCurrentSession() {
647
+ if (!currentSession) {
648
+ renderList();
649
+ return Promise.resolve();
650
+ }
651
+
652
+ statusEl.textContent = 'refreshing...';
653
+ return api('/sessions/' + encodeURIComponent(currentSession.sessionId)).then(function(data) {
654
+ statusEl.textContent = '';
655
+ if (!data) return;
656
+ currentSession.report = data.report;
657
+ currentSession.logCount = data.logCount;
658
+ currentSession.receivedAt = data.receivedAt;
659
+ rerenderVisibleLogs();
660
+ updateTabCounts();
661
+ }).catch(function(err) {
662
+ statusEl.textContent = '';
663
+ showToast('Refresh failed: ' + err.message);
664
+ });
665
+ }
666
+
667
+ function refreshCurrentView() {
668
+ return refreshCurrentSession();
669
+ }
670
+
671
+ window.refresh = function() { refreshCurrentView(); };
672
+
673
+ window.filterType = function(btn, type) {
674
+ document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
675
+ btn.classList.add('active');
676
+ window._currentFilterType = type;
677
+ applyFilters();
678
+ };
679
+
680
+ window.toggleFailed = function() {
681
+ var el = document.getElementById('failedToggle');
682
+ window._failedOnly = !window._failedOnly;
683
+ if (window._failedOnly) { el.classList.add('on'); } else { el.classList.remove('on'); }
684
+ applyFilters();
685
+ };
686
+
687
+ window.applyFilters = function() {
688
+ if (!currentSession) return;
689
+ expandedRows = {};
690
+ rerenderVisibleLogs();
691
+ };
692
+
693
+ window.toggleRow = function(rowId) {
694
+ var entry = document.getElementById('entry-' + rowId);
695
+ var json = document.getElementById('json-' + rowId);
696
+ var expand = entry ? entry.querySelector('.log-expand') : null;
697
+ if (!entry || !json) return;
698
+ expandedRows[rowId] = !expandedRows[rowId];
699
+ if (expandedRows[rowId]) {
700
+ entry.classList.add('expanded');
701
+ json.classList.add('open');
702
+ if (expand) expand.innerHTML = '&#9660;';
703
+ } else {
704
+ entry.classList.remove('expanded');
705
+ json.classList.remove('open');
706
+ if (expand) expand.innerHTML = '&#9654;';
707
+ }
708
+ };
709
+
710
+ window.copyJSON = function() {
711
+ if (!currentSession) return;
712
+ var text = JSON.stringify(currentSession.report, null, 2);
713
+ navigator.clipboard.writeText(text).then(function() {
714
+ showToast('Copied to clipboard');
715
+ }).catch(function() {
716
+ var ta = document.createElement('textarea');
717
+ ta.value = text;
718
+ ta.style.position = 'fixed';
719
+ ta.style.opacity = '0';
720
+ document.body.appendChild(ta);
721
+ ta.select();
722
+ document.execCommand('copy');
723
+ document.body.removeChild(ta);
724
+ showToast('Copied to clipboard');
725
+ });
726
+ };
727
+
728
+ // --- Routing ---
729
+
730
+ window._currentFilterType = '';
731
+ window._failedOnly = false;
732
+
733
+ function route() {
734
+ var hash = location.hash.replace('#', '');
735
+ if (hash.startsWith('session/')) {
736
+ renderDetail(decodeURIComponent(hash.substring(8)));
737
+ } else {
738
+ renderList();
739
+ }
740
+ }
741
+
742
+ // --- Incremental DOM updates ---
743
+
744
+ function isFailedEntry(e) {
745
+ return e && typeof e === 'object' && (
746
+ Boolean(e.error) ||
747
+ e.level === 'error' ||
748
+ (e.response && (e.response.success === false || e.response.status >= 400))
749
+ );
750
+ }
751
+
752
+ function renderSessionTags(logCount) {
753
+ var html = '';
754
+ Object.entries(logCount || {}).forEach(function(e) {
755
+ var type = String(e[0]);
756
+ html += '<span class="tag tag-' + toKeyPart(type) + '">' + escapeHtml(type.substring(0,3)) + ' ' + escapeHtml(String(e[1])) + '</span>';
757
+ });
758
+ return html;
759
+ }
760
+
761
+ function appendSessionCard(payload) {
762
+ var grid = document.querySelector('.session-grid');
763
+ if (!grid) { renderList(); return; }
764
+
765
+ var sid = payload.sessionId;
766
+ var existing = grid.querySelector('[data-sid="' + CSS.escape(sid) + '"]');
767
+ if (existing) {
768
+ var tags = existing.querySelector('.session-tags');
769
+ if (tags) tags.innerHTML = renderSessionTags(payload.logCount || {});
770
+ return;
771
+ }
772
+
773
+ var lc = payload.logCount || {};
774
+ var card = document.createElement('div');
775
+ card.className = 'session-card';
776
+ card.setAttribute('data-sid', sid);
777
+ card.setAttribute('onclick', "location.hash='session/" + encodeURIComponent(sid) + "'");
778
+ var html = '<div><div class="session-id">' + escapeHtml(sid) + '</div>';
779
+ html += '<div class="session-time">just now</div></div>';
780
+ html += '<div class="session-tags">' + renderSessionTags(lc) + '</div>';
781
+ html += '<div class="session-arrow">&rsaquo;</div>';
782
+ card.innerHTML = html;
783
+ grid.prepend(card);
784
+
785
+ // Update session count
786
+ var titleEl = document.querySelector('.section-title');
787
+ if (titleEl) {
788
+ var count = grid.querySelectorAll('.session-card').length;
789
+ titleEl.innerHTML = 'Sessions <span style="color:var(--text3);font-weight:400">(' + count + ')</span>';
790
+ }
791
+ }
792
+
793
+ function buildLogEntryHtml(entry, rowId, type) {
794
+ var lt = type || getLogType(entry);
795
+ var typeClass = toKeyPart(lt);
796
+ var html = '<div class="log-type log-type-' + typeClass + '">' + escapeHtml(lt.substring(0,4)) + '</div>';
797
+ html += '<div class="log-summary">' + summarize(entry) + '</div>';
798
+ html += '<div class="log-status">' + statusBadge(entry) + '</div>';
799
+ html += '<div class="log-expand">&#9654;</div>';
800
+ html += '<div class="log-json" id="json-' + rowId + '"><pre>' + escapeHtml(JSON.stringify(entry, null, 2)) + '</pre></div>';
801
+ return html;
802
+ }
803
+
804
+ function appendDeltaLogs(deltaLogs) {
805
+ var container = document.getElementById('logsContainer');
806
+ if (!container) return;
807
+
808
+ var list = container.querySelector('.log-list');
809
+ if (!list) {
810
+ applyFilters();
811
+ return;
812
+ }
813
+
814
+ var type = window._currentFilterType || '';
815
+ var failedOnly = window._failedOnly || false;
816
+ var limit = document.getElementById('limitInput') ? parseInt(document.getElementById('limitInput').value, 10) || 50 : 50;
817
+ var allNewEntries = [];
818
+
819
+ Object.entries(deltaLogs).forEach(function(entry) {
820
+ var t = entry[0];
821
+ var entries = entry[1];
822
+ if (!Array.isArray(entries)) return;
823
+ entries.forEach(function(e) {
824
+ if (failedOnly && !isFailedEntry(e)) return;
825
+ if (type && t !== type) return;
826
+ allNewEntries.push({ type: t, entry: e });
827
+ });
828
+ });
829
+
830
+ var count = list.querySelectorAll('.log-entry').length;
831
+ allNewEntries.forEach(function(item) {
832
+ var entry = item.entry;
833
+ var rowId = getLogEntryKey(entry, item.type, count++);
834
+ var div = document.createElement('div');
835
+ div.className = 'log-entry';
836
+ div.id = 'entry-' + rowId;
837
+ div.setAttribute('onclick', "toggleRow('" + rowId + "')");
838
+ div.innerHTML = buildLogEntryHtml(entry, rowId, item.type);
839
+ list.appendChild(div);
840
+ });
841
+
842
+ // Trim from top if over limit, skip expanded entries
843
+ var entries = list.querySelectorAll('.log-entry');
844
+ while (entries.length > limit) {
845
+ var first = entries[0];
846
+ if (first && first.classList.contains('expanded')) break;
847
+ list.removeChild(first);
848
+ entries = list.querySelectorAll('.log-entry');
849
+ }
850
+ }
851
+
852
+ function updateTabCounts() {
853
+ if (!currentSession) return;
854
+ var logs = currentSession.report ? currentSession.report.logs : {};
855
+ var totalLogs = Object.values(logs).reduce(function(a, v) { return a + (Array.isArray(v) ? v.length : 0); }, 0);
856
+
857
+ document.querySelectorAll('.tab').forEach(function(tab) {
858
+ var t = tab.getAttribute('data-type') || '';
859
+ var countEl = tab.querySelector('.count');
860
+ var c = t ? (logs[t] ? logs[t].length : 0) : totalLogs;
861
+ if (countEl) countEl.textContent = c;
862
+ });
863
+
864
+ document.querySelectorAll('.detail-meta-item[data-type]').forEach(function(el) {
865
+ var t = el.getAttribute('data-type');
866
+ var strong = el.querySelector('strong');
867
+ if (!strong) return;
868
+ var c = t === 'Entries' ? totalLogs : (logs[t] ? logs[t].length : 0);
869
+ el.innerHTML = '<strong>' + strong.textContent + '</strong> ' + c;
870
+ });
871
+ }
872
+
873
+ // --- SSE ---
874
+
875
+ var sseConnected = false;
876
+ var eventSource = null;
877
+
878
+ function connectSSE() {
879
+ if (eventSource) { try { eventSource.close(); } catch {} }
880
+ eventSource = new EventSource(withAuth('/events'));
881
+
882
+ eventSource.addEventListener('logs', function(e) {
883
+ try {
884
+ var payload = JSON.parse(e.data);
885
+ sseConnected = true;
886
+ pulseDot.style.background = 'var(--cyan)';
887
+ pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
888
+
889
+ // Session list page — append new session card
890
+ if (!currentSession) {
891
+ appendSessionCard(payload);
892
+ return;
893
+ }
894
+
895
+ // Detail page — merge delta if same session
896
+ if (payload.sessionId === currentSession.sessionId) {
897
+ if (payload.type === 'delta' && payload.delta) {
898
+ var deltaLogs = payload.delta.logs || {};
899
+ var report = currentSession.report || { version: 2, logs: {} };
900
+ if (!report.logs) report.logs = {};
901
+
902
+ Object.entries(deltaLogs).forEach(function(entry) {
903
+ var type = entry[0];
904
+ var entries = entry[1];
905
+ if (!Array.isArray(entries)) return;
906
+ if (!report.logs[type]) report.logs[type] = [];
907
+ report.logs[type] = report.logs[type].concat(entries);
908
+ });
909
+
910
+ currentSession.report = report;
911
+ if (payload.logCount) currentSession.logCount = payload.logCount;
912
+ appendDeltaLogs(deltaLogs);
913
+ updateTabCounts();
914
+ } else if (payload.type === 'full') {
915
+ refreshCurrentSession();
916
+ }
917
+ } else if (!location.hash.startsWith('session/')) {
918
+ appendSessionCard(payload);
919
+ }
920
+ } catch {}
921
+ });
922
+
923
+ eventSource.onerror = function() {
924
+ sseConnected = false;
925
+ pulseDot.style.background = 'var(--amber)';
926
+ pulseDot.style.boxShadow = '0 0 8px var(--amber)';
927
+ };
928
+ }
929
+
930
+ window.addEventListener('hashchange', route);
931
+ connectSSE();
932
+ route();
933
+ })();
934
+ </script>
935
+ </body>
936
+ </html>