react-native-debug-toolkit 2.3.0 → 3.1.2
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 +115 -97
- package/README.zh-CN.md +113 -95
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/core/initialize.js +5 -0
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/network/index.js +28 -2
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +14 -6
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +56 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/DaemonClient.js +721 -0
- package/lib/commonjs/utils/DaemonClient.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +132 -0
- package/lib/commonjs/utils/deviceReport.js.map +1 -0
- package/lib/module/core/initialize.js +6 -0
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/network/index.js +25 -1
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +14 -6
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/DaemonClient.js +703 -0
- package/lib/module/utils/DaemonClient.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/deviceReport.js +128 -0
- package/lib/module/utils/deviceReport.js.map +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/network/index.d.ts +2 -0
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
- package/node/daemon/src/cli.js +82 -0
- package/node/daemon/src/console/console.html +1662 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +38 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +447 -0
- package/node/daemon/src/store.js +187 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +96 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +8 -3
- package/src/core/initialize.ts +8 -0
- package/src/features/network/index.ts +30 -3
- package/src/features/network/networkInterceptor.ts +19 -6
- package/src/index.ts +22 -0
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/DaemonClient.ts +887 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/deviceReport.ts +203 -0
|
@@ -0,0 +1,1662 @@
|
|
|
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
|
+
--purple:#7c4dff;--purple-dim:rgba(124,77,255,.12);
|
|
19
|
+
--pink:#e040fb;--pink-dim:rgba(224,64,251,.12);
|
|
20
|
+
--font-mono:'SF Mono',Monaco,Consolas,monospace;
|
|
21
|
+
--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
|
22
|
+
--radius:6px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
html{scrollbar-width:thin;scrollbar-color:var(--border2) transparent}
|
|
26
|
+
::-webkit-scrollbar{width:6px;height:6px}
|
|
27
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
28
|
+
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|
|
29
|
+
|
|
30
|
+
body{
|
|
31
|
+
font-family:var(--font-sans);background:var(--bg);color:var(--text);
|
|
32
|
+
line-height:1.6;min-height:100vh;
|
|
33
|
+
background-image:
|
|
34
|
+
repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,229,255,.012) 2px,rgba(0,229,255,.012) 4px);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body::after{
|
|
38
|
+
content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;
|
|
39
|
+
background:repeating-linear-gradient(0deg,transparent 0px,transparent 1px,rgba(0,0,0,.03) 1px,rgba(0,0,0,.03) 2px);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
a{color:var(--cyan);text-decoration:none;transition:color .15s}
|
|
43
|
+
a:hover{color:#fff}
|
|
44
|
+
|
|
45
|
+
/* Header */
|
|
46
|
+
header{
|
|
47
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
48
|
+
padding:0 24px;height:56px;
|
|
49
|
+
background:linear-gradient(180deg,var(--bg2) 0%,rgba(13,18,32,.95) 100%);
|
|
50
|
+
border-bottom:1px solid var(--border);
|
|
51
|
+
position:sticky;top:0;z-index:100;
|
|
52
|
+
backdrop-filter:blur(12px);
|
|
53
|
+
}
|
|
54
|
+
.header-left{display:flex;align-items:center;gap:14px}
|
|
55
|
+
.pulse{
|
|
56
|
+
width:8px;height:8px;border-radius:50%;background:var(--cyan);flex-shrink:0;
|
|
57
|
+
box-shadow:0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3);
|
|
58
|
+
animation:pulse 2s ease-in-out infinite;
|
|
59
|
+
}
|
|
60
|
+
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}}
|
|
61
|
+
header h1{font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--text)}
|
|
62
|
+
header h1 span{color:var(--text3);font-weight:400}
|
|
63
|
+
.header-right{display:flex;align-items:center;gap:10px}
|
|
64
|
+
.header-meta{font-size:11px;font-family:var(--font-mono);color:var(--text3)}
|
|
65
|
+
|
|
66
|
+
/* Buttons */
|
|
67
|
+
.btn{
|
|
68
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
69
|
+
padding:6px 14px;font-size:12px;font-weight:500;
|
|
70
|
+
border:1px solid var(--border2);border-radius:var(--radius);
|
|
71
|
+
background:var(--surface2);color:var(--text2);cursor:pointer;
|
|
72
|
+
font-family:var(--font-sans);transition:all .15s;letter-spacing:.01em;
|
|
73
|
+
}
|
|
74
|
+
.btn:hover{background:var(--surface3);color:var(--text);border-color:var(--cyan-mid)}
|
|
75
|
+
.btn:active{transform:scale(.97)}
|
|
76
|
+
.btn-icon{padding:6px 10px;font-size:14px}
|
|
77
|
+
.btn-sm{padding:3px 8px;font-size:10px;border-radius:3px}
|
|
78
|
+
.btn-ghost{background:transparent;border-color:transparent}
|
|
79
|
+
.btn-ghost:hover{background:var(--surface2);border-color:var(--border)}
|
|
80
|
+
|
|
81
|
+
/* Container */
|
|
82
|
+
.container{max-width:1160px;margin:0 auto;padding:28px 24px}
|
|
83
|
+
|
|
84
|
+
/* Empty state */
|
|
85
|
+
.empty{text-align:center;padding:80px 20px;color:var(--text3)}
|
|
86
|
+
.empty-icon{font-size:40px;margin-bottom:16px;opacity:.3;font-family:var(--font-mono)}
|
|
87
|
+
.empty p{font-size:14px;line-height:1.8}
|
|
88
|
+
.empty code{
|
|
89
|
+
font-family:var(--font-mono);font-size:12px;
|
|
90
|
+
background:var(--surface);padding:2px 8px;border-radius:3px;
|
|
91
|
+
border:1px solid var(--border);color:var(--cyan);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Device list */
|
|
95
|
+
.section-title{
|
|
96
|
+
font-size:13px;font-weight:600;color:var(--text2);
|
|
97
|
+
text-transform:uppercase;letter-spacing:.08em;
|
|
98
|
+
margin-bottom:16px;display:flex;align-items:center;gap:8px;
|
|
99
|
+
}
|
|
100
|
+
.section-title::after{content:'';flex:1;height:1px;background:var(--border)}
|
|
101
|
+
|
|
102
|
+
.device-grid{display:flex;flex-direction:column;gap:6px}
|
|
103
|
+
.device-card{
|
|
104
|
+
display:grid;grid-template-columns:minmax(0,1.2fr) minmax(170px,.8fr) auto 20px;align-items:center;gap:16px;
|
|
105
|
+
padding:14px 18px;background:var(--surface);
|
|
106
|
+
border:1px solid var(--border);border-radius:var(--radius);
|
|
107
|
+
cursor:pointer;transition:all .15s;
|
|
108
|
+
}
|
|
109
|
+
.device-card:hover{
|
|
110
|
+
background:var(--surface2);border-color:var(--border2);
|
|
111
|
+
transform:translateX(3px);
|
|
112
|
+
box-shadow:0 0 0 1px var(--cyan-dim),0 4px 16px rgba(0,0,0,.3);
|
|
113
|
+
}
|
|
114
|
+
.device-id{font-family:var(--font-mono);font-size:12px;color:var(--text);font-weight:500}
|
|
115
|
+
.device-time{font-size:11px;color:var(--text3);font-family:var(--font-mono);margin-top:2px}
|
|
116
|
+
.device-title{font-size:13px;color:var(--text);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
117
|
+
.device-subtitle{font-family:var(--font-mono);font-size:11px;color:var(--text2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
118
|
+
.device-meta-group{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
119
|
+
.device-meta-line{font-family:var(--font-mono);font-size:11px;color:var(--text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
120
|
+
.device-meta-line strong{font-family:var(--font-sans);font-weight:600;color:var(--text2);margin-right:5px}
|
|
121
|
+
.device-tags{display:flex;gap:6px;flex-wrap:wrap}
|
|
122
|
+
.tag{
|
|
123
|
+
font-family:var(--font-mono);font-size:10px;font-weight:500;
|
|
124
|
+
padding:2px 8px;border-radius:3px;
|
|
125
|
+
background:var(--cyan-dim);color:var(--cyan);
|
|
126
|
+
border:1px solid rgba(0,229,255,.1);
|
|
127
|
+
}
|
|
128
|
+
.tag-network{background:var(--cyan-dim);color:var(--cyan);border-color:rgba(0,229,255,.1)}
|
|
129
|
+
.tag-console{background:var(--green-dim);color:var(--green);border-color:rgba(0,230,118,.1)}
|
|
130
|
+
.tag-error{background:var(--red-dim);color:var(--red);border-color:rgba(255,23,68,.1)}
|
|
131
|
+
.tag-warn{background:var(--amber-dim);color:var(--amber);border-color:rgba(255,171,0,.1)}
|
|
132
|
+
.tag-track{background:var(--orange-dim);color:var(--orange);border-color:rgba(255,110,64,.1)}
|
|
133
|
+
.device-arrow{color:var(--text3);font-size:16px;transition:color .15s}
|
|
134
|
+
.device-card:hover .device-arrow{color:var(--cyan)}
|
|
135
|
+
|
|
136
|
+
/* Detail view */
|
|
137
|
+
.back-link{
|
|
138
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
139
|
+
font-size:12px;color:var(--text3);margin-bottom:20px;
|
|
140
|
+
padding:6px 0;transition:color .15s;
|
|
141
|
+
}
|
|
142
|
+
.back-link:hover{color:var(--cyan)}
|
|
143
|
+
.back-link svg{width:14px;height:14px}
|
|
144
|
+
|
|
145
|
+
.detail-header{
|
|
146
|
+
background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
|
147
|
+
padding:20px 24px;margin-bottom:20px;
|
|
148
|
+
border-left:3px solid var(--cyan);
|
|
149
|
+
}
|
|
150
|
+
.detail-id{font-family:var(--font-mono);font-size:13px;color:var(--cyan);font-weight:500}
|
|
151
|
+
.detail-meta{
|
|
152
|
+
display:flex;gap:20px;margin-top:10px;flex-wrap:wrap;
|
|
153
|
+
}
|
|
154
|
+
.detail-meta-item{font-size:11px;color:var(--text3);font-family:var(--font-mono)}
|
|
155
|
+
.detail-meta-item strong{color:var(--text2);font-weight:500}
|
|
156
|
+
|
|
157
|
+
/* Tabs */
|
|
158
|
+
.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}
|
|
159
|
+
.tab{
|
|
160
|
+
padding:7px 16px;font-size:12px;font-weight:500;
|
|
161
|
+
border:none;border-radius:4px;background:transparent;
|
|
162
|
+
color:var(--text3);cursor:pointer;font-family:var(--font-sans);
|
|
163
|
+
transition:all .15s;white-space:nowrap;letter-spacing:.01em;
|
|
164
|
+
}
|
|
165
|
+
.tab:hover{color:var(--text2);background:var(--surface)}
|
|
166
|
+
.tab.active{
|
|
167
|
+
background:var(--cyan);color:var(--bg);font-weight:600;
|
|
168
|
+
box-shadow:0 0 12px rgba(0,229,255,.25);
|
|
169
|
+
}
|
|
170
|
+
.tab .count{font-weight:400;opacity:.7;margin-left:3px;font-size:11px}
|
|
171
|
+
|
|
172
|
+
/* Toolbar */
|
|
173
|
+
.toolbar{
|
|
174
|
+
display:flex;align-items:center;gap:14px;margin-bottom:18px;
|
|
175
|
+
padding:10px 16px;background:var(--bg2);border-radius:var(--radius);
|
|
176
|
+
border:1px solid var(--border);flex-wrap:wrap;
|
|
177
|
+
}
|
|
178
|
+
.toolbar label{
|
|
179
|
+
font-size:12px;color:var(--text2);display:flex;align-items:center;gap:6px;
|
|
180
|
+
font-family:var(--font-mono);cursor:pointer;
|
|
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
|
+
.toggle{
|
|
189
|
+
position:relative;width:32px;height:18px;
|
|
190
|
+
background:var(--surface3);border-radius:9px;cursor:pointer;
|
|
191
|
+
transition:background .15s;border:1px solid var(--border2);
|
|
192
|
+
}
|
|
193
|
+
.toggle.on{background:var(--red);border-color:var(--red)}
|
|
194
|
+
.toggle::after{
|
|
195
|
+
content:'';position:absolute;top:2px;left:2px;
|
|
196
|
+
width:12px;height:12px;border-radius:50%;background:#fff;
|
|
197
|
+
transition:transform .15s;
|
|
198
|
+
}
|
|
199
|
+
.toggle.on::after{transform:translateX(14px)}
|
|
200
|
+
|
|
201
|
+
/* Search */
|
|
202
|
+
.search-wrap{
|
|
203
|
+
flex:1;min-width:160px;position:relative;
|
|
204
|
+
}
|
|
205
|
+
.search-input{
|
|
206
|
+
width:100%;padding:5px 10px 5px 28px;
|
|
207
|
+
background:var(--surface);border:1px solid var(--border2);border-radius:4px;
|
|
208
|
+
color:var(--text);font-size:12px;font-family:var(--font-mono);
|
|
209
|
+
transition:border-color .15s;
|
|
210
|
+
}
|
|
211
|
+
.search-input::placeholder{color:var(--text3)}
|
|
212
|
+
.search-input:focus{outline:none;border-color:var(--cyan)}
|
|
213
|
+
.search-icon{
|
|
214
|
+
position:absolute;left:8px;top:50%;transform:translateY(-50%);
|
|
215
|
+
color:var(--text3);font-size:13px;pointer-events:none;
|
|
216
|
+
}
|
|
217
|
+
.search-clear{
|
|
218
|
+
position:absolute;right:6px;top:50%;transform:translateY(-50%);
|
|
219
|
+
background:none;border:none;color:var(--text3);cursor:pointer;
|
|
220
|
+
font-size:14px;padding:2px 4px;line-height:1;
|
|
221
|
+
display:none;
|
|
222
|
+
}
|
|
223
|
+
.search-clear.visible{display:block}
|
|
224
|
+
.search-clear:hover{color:var(--text)}
|
|
225
|
+
.kbd{
|
|
226
|
+
font-family:var(--font-mono);font-size:9px;color:var(--text3);
|
|
227
|
+
background:var(--surface2);border:1px solid var(--border2);
|
|
228
|
+
padding:1px 4px;border-radius:2px;letter-spacing:.02em;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Curl help - collapsible */
|
|
232
|
+
.curl-panel{
|
|
233
|
+
margin-bottom:18px;padding:12px 14px;background:var(--bg2);
|
|
234
|
+
border:1px solid var(--border);border-radius:var(--radius);
|
|
235
|
+
}
|
|
236
|
+
.curl-header{
|
|
237
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
238
|
+
cursor:pointer;user-select:none;
|
|
239
|
+
}
|
|
240
|
+
.curl-title{
|
|
241
|
+
font-size:11px;font-family:var(--font-mono);font-weight:700;
|
|
242
|
+
color:var(--cyan);text-transform:uppercase;letter-spacing:.08em;
|
|
243
|
+
}
|
|
244
|
+
.curl-toggle{
|
|
245
|
+
font-size:10px;color:var(--text3);transition:transform .2s;
|
|
246
|
+
}
|
|
247
|
+
.curl-toggle.open{transform:rotate(90deg)}
|
|
248
|
+
.curl-body{display:none;margin-top:10px}
|
|
249
|
+
.curl-body.open{display:block}
|
|
250
|
+
.curl-list{display:flex;flex-direction:column;gap:6px}
|
|
251
|
+
.curl-list code{
|
|
252
|
+
display:block;padding:7px 9px;background:rgba(0,0,0,.18);
|
|
253
|
+
border:1px solid rgba(42,63,102,.65);border-radius:4px;
|
|
254
|
+
color:var(--text2);font-family:var(--font-mono);font-size:11px;
|
|
255
|
+
white-space:pre-wrap;word-break:break-word;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Log entries - redesigned */
|
|
259
|
+
.log-list{display:flex;flex-direction:column;gap:2px}
|
|
260
|
+
.log-entry{
|
|
261
|
+
background:var(--surface);border:1px solid transparent;border-radius:var(--radius);
|
|
262
|
+
cursor:pointer;transition:all .15s;overflow:hidden;
|
|
263
|
+
animation:fadeSlideIn .3s ease-out both;
|
|
264
|
+
}
|
|
265
|
+
@keyframes fadeSlideIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
266
|
+
.log-entry:hover{
|
|
267
|
+
background:var(--surface2);border-color:var(--border2);
|
|
268
|
+
box-shadow:0 2px 8px rgba(0,0,0,.2);
|
|
269
|
+
}
|
|
270
|
+
.log-entry.expanded{
|
|
271
|
+
border-color:var(--cyan-mid);background:var(--surface2);
|
|
272
|
+
box-shadow:0 2px 12px rgba(0,0,0,.25);
|
|
273
|
+
}
|
|
274
|
+
.log-entry.focused{outline:1px solid var(--cyan);outline-offset:-1px}
|
|
275
|
+
|
|
276
|
+
.log-row{
|
|
277
|
+
display:grid;grid-template-columns:90px 1fr auto auto 28px;
|
|
278
|
+
align-items:center;gap:0;
|
|
279
|
+
padding:0 4px 0 0;min-height:46px;
|
|
280
|
+
}
|
|
281
|
+
.log-type{
|
|
282
|
+
font-family:var(--font-mono);font-size:10px;font-weight:600;
|
|
283
|
+
text-transform:uppercase;letter-spacing:.06em;
|
|
284
|
+
padding:0 12px;height:100%;display:flex;align-items:center;
|
|
285
|
+
justify-content:center;text-align:center;
|
|
286
|
+
}
|
|
287
|
+
.log-type-network{color:var(--cyan)}
|
|
288
|
+
.log-type-console{color:var(--green)}
|
|
289
|
+
.log-type-navigation{color:var(--purple)}
|
|
290
|
+
.log-type-track{color:var(--orange)}
|
|
291
|
+
.log-type-zustand{color:var(--pink)}
|
|
292
|
+
.log-type-unknown{color:var(--text3)}
|
|
293
|
+
|
|
294
|
+
.log-summary-col{
|
|
295
|
+
padding:8px 12px;min-width:0;overflow:hidden;
|
|
296
|
+
}
|
|
297
|
+
.log-summary{
|
|
298
|
+
font-family:var(--font-mono);font-size:12px;color:var(--text);
|
|
299
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
|
|
300
|
+
overflow:hidden;line-height:1.5;word-break:break-all;
|
|
301
|
+
}
|
|
302
|
+
.log-timestamp{
|
|
303
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text3);
|
|
304
|
+
margin-top:2px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.log-status{
|
|
308
|
+
padding:0 8px;height:100%;display:flex;align-items:center;justify-content:center;
|
|
309
|
+
}
|
|
310
|
+
.badge{
|
|
311
|
+
font-family:var(--font-mono);font-size:10px;font-weight:600;
|
|
312
|
+
padding:2px 8px;border-radius:3px;letter-spacing:.02em;white-space:nowrap;
|
|
313
|
+
}
|
|
314
|
+
.badge-ok{background:var(--green-dim);color:var(--green)}
|
|
315
|
+
.badge-error{background:var(--red-dim);color:var(--red)}
|
|
316
|
+
.badge-warn{background:var(--amber-dim);color:var(--amber)}
|
|
317
|
+
.badge-info{background:var(--cyan-dim);color:var(--cyan)}
|
|
318
|
+
|
|
319
|
+
.log-copy{
|
|
320
|
+
padding:0 4px;height:100%;display:flex;align-items:center;justify-content:center;
|
|
321
|
+
opacity:0;transition:opacity .15s;
|
|
322
|
+
}
|
|
323
|
+
.log-entry:hover .log-copy{opacity:1}
|
|
324
|
+
.copy-btn{
|
|
325
|
+
background:none;border:1px solid transparent;cursor:pointer;
|
|
326
|
+
color:var(--text3);font-size:13px;padding:2px 4px;border-radius:3px;
|
|
327
|
+
transition:all .15s;
|
|
328
|
+
}
|
|
329
|
+
.copy-btn:hover{color:var(--cyan);background:var(--cyan-dim);border-color:rgba(0,229,255,.15)}
|
|
330
|
+
|
|
331
|
+
.log-expand{
|
|
332
|
+
padding:0 8px;height:100%;display:flex;align-items:center;justify-content:center;
|
|
333
|
+
color:var(--text3);font-size:10px;transition:transform .2s,color .15s;
|
|
334
|
+
}
|
|
335
|
+
.log-entry:hover .log-expand{color:var(--cyan)}
|
|
336
|
+
.log-entry.expanded .log-expand{transform:rotate(90deg);color:var(--cyan)}
|
|
337
|
+
|
|
338
|
+
/* Expanded detail panel */
|
|
339
|
+
.log-detail{
|
|
340
|
+
display:none;border-top:1px solid var(--border);
|
|
341
|
+
animation:detailFadeIn .2s ease-out;
|
|
342
|
+
}
|
|
343
|
+
.log-entry.expanded .log-detail{display:block}
|
|
344
|
+
@keyframes detailFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
|
345
|
+
|
|
346
|
+
.log-detail-inner{padding:16px 18px}
|
|
347
|
+
|
|
348
|
+
/* Detail sections */
|
|
349
|
+
.detail-sections{display:flex;flex-direction:column;gap:10px}
|
|
350
|
+
.detail-section{
|
|
351
|
+
border:1px solid var(--border);border-radius:var(--radius);
|
|
352
|
+
background:rgba(8,12,22,.35);overflow:hidden;
|
|
353
|
+
}
|
|
354
|
+
.detail-section-header{
|
|
355
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
356
|
+
padding:7px 12px;border-bottom:1px solid var(--border);
|
|
357
|
+
background:rgba(0,229,255,.03);
|
|
358
|
+
}
|
|
359
|
+
.detail-section-title{
|
|
360
|
+
font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
|
|
361
|
+
color:var(--cyan);font-family:var(--font-mono);
|
|
362
|
+
}
|
|
363
|
+
.detail-section-copy{
|
|
364
|
+
background:none;border:none;cursor:pointer;
|
|
365
|
+
color:var(--text3);font-size:11px;padding:1px 4px;border-radius:2px;
|
|
366
|
+
transition:color .15s;
|
|
367
|
+
}
|
|
368
|
+
.detail-section-copy:hover{color:var(--cyan)}
|
|
369
|
+
.detail-section-body{padding:0}
|
|
370
|
+
|
|
371
|
+
/* Detail key-value table */
|
|
372
|
+
.detail-table{width:100%;border-collapse:collapse}
|
|
373
|
+
.detail-table tr{border-bottom:1px solid rgba(30,45,74,.5)}
|
|
374
|
+
.detail-table tr:last-child{border-bottom:none}
|
|
375
|
+
.detail-table th,.detail-table td{
|
|
376
|
+
padding:8px 12px;vertical-align:top;font-size:12px;
|
|
377
|
+
}
|
|
378
|
+
.detail-table th{
|
|
379
|
+
width:100px;color:var(--text3);font-family:var(--font-mono);font-weight:500;
|
|
380
|
+
text-align:left;white-space:nowrap;
|
|
381
|
+
}
|
|
382
|
+
.detail-table td{color:var(--text2);font-family:var(--font-mono);word-break:break-word;line-height:1.6}
|
|
383
|
+
|
|
384
|
+
/* JSON blocks */
|
|
385
|
+
.json-block{
|
|
386
|
+
margin:0;padding:10px 12px;font-family:var(--font-mono);font-size:11px;
|
|
387
|
+
line-height:1.7;color:var(--text2);white-space:pre-wrap;word-break:break-word;
|
|
388
|
+
max-height:320px;overflow:auto;background:rgba(0,0,0,.16);
|
|
389
|
+
}
|
|
390
|
+
.json-block .json-key{color:var(--cyan)}
|
|
391
|
+
.json-block .json-string{color:var(--green)}
|
|
392
|
+
.json-block .json-number{color:var(--amber)}
|
|
393
|
+
.json-block .json-bool{color:var(--purple)}
|
|
394
|
+
.json-block .json-null{color:var(--text3)}
|
|
395
|
+
.json-compact{max-height:160px;font-size:10px;line-height:1.5;padding:8px 10px}
|
|
396
|
+
|
|
397
|
+
/* Value pills */
|
|
398
|
+
.value-list{display:flex;flex-direction:column;gap:6px;padding:10px 12px}
|
|
399
|
+
.value-pill{
|
|
400
|
+
display:block;padding:8px 10px;border:1px solid rgba(42,63,102,.6);
|
|
401
|
+
border-radius:4px;background:rgba(0,0,0,.1);font-family:var(--font-mono);
|
|
402
|
+
font-size:12px;color:var(--text2);word-break:break-word;line-height:1.6;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Network detail hero */
|
|
406
|
+
.network-hero{
|
|
407
|
+
padding:12px 14px;border-bottom:1px solid rgba(30,45,74,.5);
|
|
408
|
+
display:flex;align-items:center;gap:10px;
|
|
409
|
+
}
|
|
410
|
+
.method-badge{
|
|
411
|
+
font-family:var(--font-mono);font-size:11px;font-weight:700;
|
|
412
|
+
padding:3px 10px;border-radius:3px;letter-spacing:.04em;
|
|
413
|
+
}
|
|
414
|
+
.method-get{background:var(--cyan-dim);color:var(--cyan)}
|
|
415
|
+
.method-post{background:var(--green-dim);color:var(--green)}
|
|
416
|
+
.method-put{background:var(--amber-dim);color:var(--amber)}
|
|
417
|
+
.method-patch{background:var(--amber-dim);color:var(--amber)}
|
|
418
|
+
.method-delete{background:var(--red-dim);color:var(--red)}
|
|
419
|
+
.network-url{
|
|
420
|
+
font-family:var(--font-mono);font-size:12px;color:var(--text);
|
|
421
|
+
word-break:break-all;line-height:1.5;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Navigation hero */
|
|
425
|
+
.nav-hero{
|
|
426
|
+
padding:12px 14px;border-bottom:1px solid rgba(30,45,74,.5);
|
|
427
|
+
display:flex;align-items:center;gap:8px;
|
|
428
|
+
font-family:var(--font-mono);font-size:12px;
|
|
429
|
+
}
|
|
430
|
+
.nav-from{color:var(--text3)}
|
|
431
|
+
.nav-arrow{color:var(--cyan);font-size:14px}
|
|
432
|
+
.nav-to{color:var(--text);font-weight:500}
|
|
433
|
+
|
|
434
|
+
/* Entry footer */
|
|
435
|
+
.entry-footer{
|
|
436
|
+
display:flex;align-items:center;gap:8px;
|
|
437
|
+
padding:10px 0 0;margin-top:10px;border-top:1px solid rgba(30,45,74,.5);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Actions bar */
|
|
441
|
+
.actions{
|
|
442
|
+
display:flex;gap:8px;margin-top:20px;padding-top:16px;
|
|
443
|
+
border-top:1px solid var(--border);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* Toast */
|
|
447
|
+
.toast{
|
|
448
|
+
position:fixed;bottom:24px;right:24px;
|
|
449
|
+
padding:10px 20px;
|
|
450
|
+
background:var(--cyan);color:var(--bg);
|
|
451
|
+
border-radius:var(--radius);font-size:13px;font-weight:600;
|
|
452
|
+
font-family:var(--font-sans);
|
|
453
|
+
box-shadow:0 4px 20px rgba(0,229,255,.3);
|
|
454
|
+
transform:translateY(80px);opacity:0;transition:all .3s cubic-bezier(.2,.9,.3,1);
|
|
455
|
+
z-index:1000;
|
|
456
|
+
}
|
|
457
|
+
.toast.show{transform:translateY(0);opacity:1}
|
|
458
|
+
|
|
459
|
+
/* Stagger animations */
|
|
460
|
+
.log-entry:nth-child(1){animation-delay:.0s}
|
|
461
|
+
.log-entry:nth-child(2){animation-delay:.02s}
|
|
462
|
+
.log-entry:nth-child(3){animation-delay:.04s}
|
|
463
|
+
.log-entry:nth-child(4){animation-delay:.06s}
|
|
464
|
+
.log-entry:nth-child(5){animation-delay:.08s}
|
|
465
|
+
.log-entry:nth-child(n+6){animation-delay:.1s}
|
|
466
|
+
|
|
467
|
+
/* Device info */
|
|
468
|
+
.device-info{
|
|
469
|
+
display:flex;gap:16px;align-items:center;flex-wrap:wrap;
|
|
470
|
+
margin-top:10px;padding-top:10px;border-top:1px solid var(--border);
|
|
471
|
+
}
|
|
472
|
+
.device-badge{
|
|
473
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
474
|
+
font-family:var(--font-mono);font-size:11px;
|
|
475
|
+
padding:3px 10px;border-radius:4px;
|
|
476
|
+
background:var(--surface2);color:var(--text2);
|
|
477
|
+
border:1px solid var(--border);
|
|
478
|
+
}
|
|
479
|
+
.device-badge.platform-ios{border-color:rgba(255,255,255,.2);color:#fff}
|
|
480
|
+
.device-badge.platform-android{border-color:rgba(0,230,118,.2);color:var(--green)}
|
|
481
|
+
.live-badge{
|
|
482
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
483
|
+
font-family:var(--font-mono);font-size:11px;
|
|
484
|
+
padding:3px 10px;border-radius:4px;
|
|
485
|
+
background:var(--cyan-dim);color:var(--cyan);
|
|
486
|
+
border:1px solid rgba(0,229,255,.15);
|
|
487
|
+
animation:livePulse 2s ease-in-out infinite;
|
|
488
|
+
}
|
|
489
|
+
@keyframes livePulse{0%,100%{opacity:1}50%{opacity:.6}}
|
|
490
|
+
.live-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
|
|
491
|
+
|
|
492
|
+
/* Search highlight */
|
|
493
|
+
mark{
|
|
494
|
+
background:rgba(0,229,255,.25);color:var(--text);
|
|
495
|
+
border-radius:2px;padding:0 1px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Responsive */
|
|
499
|
+
@media(max-width:640px){
|
|
500
|
+
.container{padding:16px}
|
|
501
|
+
.device-card{grid-template-columns:1fr;gap:8px}
|
|
502
|
+
.log-row{grid-template-columns:72px 1fr auto 28px}
|
|
503
|
+
.log-copy{display:none}
|
|
504
|
+
.detail-table th{width:80px}
|
|
505
|
+
.detail-header{padding:14px 16px}
|
|
506
|
+
.tabs{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
507
|
+
.toolbar{gap:8px}
|
|
508
|
+
.search-wrap{min-width:120px;flex-basis:100%}
|
|
509
|
+
}
|
|
510
|
+
</style>
|
|
511
|
+
</head>
|
|
512
|
+
<body>
|
|
513
|
+
<header>
|
|
514
|
+
<div class="header-left">
|
|
515
|
+
<div class="pulse" id="pulseDot"></div>
|
|
516
|
+
<h1>Debug Toolkit <span>Console</span></h1>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="header-right">
|
|
519
|
+
<span class="header-meta" id="status"></span>
|
|
520
|
+
<span class="header-meta" id="ipHint" style="color:var(--text2);display:none"></span>
|
|
521
|
+
<button class="btn" onclick="refresh()">
|
|
522
|
+
<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>
|
|
523
|
+
Refresh
|
|
524
|
+
</button>
|
|
525
|
+
</div>
|
|
526
|
+
</header>
|
|
527
|
+
<div class="container" id="app"></div>
|
|
528
|
+
<div class="toast" id="toast"></div>
|
|
529
|
+
|
|
530
|
+
<script>
|
|
531
|
+
(function() {
|
|
532
|
+
'use strict';
|
|
533
|
+
|
|
534
|
+
var app = document.getElementById('app');
|
|
535
|
+
var statusEl = document.getElementById('status');
|
|
536
|
+
var toastEl = document.getElementById('toast');
|
|
537
|
+
var pulseDot = document.getElementById('pulseDot');
|
|
538
|
+
var currentDevice = null;
|
|
539
|
+
var expandedRows = {};
|
|
540
|
+
var authToken = null;
|
|
541
|
+
var searchTerm = '';
|
|
542
|
+
var focusedIndex = -1;
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
var params = new URLSearchParams(location.search);
|
|
546
|
+
authToken = params.get('token') || localStorage.getItem('debugToolkitToken');
|
|
547
|
+
if (authToken) localStorage.setItem('debugToolkitToken', authToken);
|
|
548
|
+
} catch {
|
|
549
|
+
authToken = null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function withAuth(path) {
|
|
553
|
+
if (!authToken) return path;
|
|
554
|
+
var join = path.indexOf('?') >= 0 ? '&' : '?';
|
|
555
|
+
return path + join + 'token=' + encodeURIComponent(authToken);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function api(path) {
|
|
559
|
+
return fetch(withAuth(path)).then(function(r) {
|
|
560
|
+
return r.json().then(function(body) {
|
|
561
|
+
if (!r.ok) throw new Error(body && body.error ? body.error : ('HTTP ' + r.status));
|
|
562
|
+
return body;
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function absoluteUrl(path) {
|
|
568
|
+
return location.origin + withAuth(path);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function curlCommand(path) {
|
|
572
|
+
return "curl '" + absoluteUrl(path).replace(/'/g, "'\\''") + "'";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderCurlPanel(title, commands) {
|
|
576
|
+
var id = 'curl-' + Math.random().toString(36).slice(2,8);
|
|
577
|
+
var html = '<div class="curl-panel">';
|
|
578
|
+
html += '<div class="curl-header" onclick="toggleCurl(\'' + id + '\')">';
|
|
579
|
+
html += '<div class="curl-title">' + escapeHtml(title) + '</div>';
|
|
580
|
+
html += '<span class="curl-toggle" id="toggle-' + id + '">▶</span>';
|
|
581
|
+
html += '</div>';
|
|
582
|
+
html += '<div class="curl-body" id="' + id + '"><div class="curl-list">';
|
|
583
|
+
commands.forEach(function(command) {
|
|
584
|
+
html += '<code>' + escapeHtml(command) + '</code>';
|
|
585
|
+
});
|
|
586
|
+
html += '</div></div></div>';
|
|
587
|
+
return html;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function showToast(msg) {
|
|
591
|
+
toastEl.textContent = msg;
|
|
592
|
+
toastEl.classList.add('show');
|
|
593
|
+
setTimeout(function() { toastEl.classList.remove('show'); }, 2000);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function escapeHtml(s) {
|
|
597
|
+
var d = document.createElement('div');
|
|
598
|
+
d.textContent = s;
|
|
599
|
+
return d.innerHTML;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function formatTime(iso) {
|
|
603
|
+
if (!iso) return '-';
|
|
604
|
+
try {
|
|
605
|
+
var d = new Date(iso);
|
|
606
|
+
var pad = function(n) { return String(n).padStart(2, '0'); };
|
|
607
|
+
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) +
|
|
608
|
+
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
|
609
|
+
} catch { return iso; }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatTimeShort(iso) {
|
|
613
|
+
if (!iso) return '';
|
|
614
|
+
try {
|
|
615
|
+
var d = new Date(iso);
|
|
616
|
+
var pad = function(n) { return String(n).padStart(2, '0'); };
|
|
617
|
+
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) + '.' + String(d.getMilliseconds()).padStart(3,'0').slice(0,2);
|
|
618
|
+
} catch { return iso; }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function readObject(value) {
|
|
622
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function readTimestamp(entry) {
|
|
626
|
+
if (!entry || typeof entry !== 'object') return 0;
|
|
627
|
+
var value = entry.timestamp || entry.time || entry.createdAt;
|
|
628
|
+
if (typeof value === 'number') return value;
|
|
629
|
+
if (typeof value === 'string') {
|
|
630
|
+
var parsed = Date.parse(value);
|
|
631
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
632
|
+
}
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function labelForType(type) {
|
|
637
|
+
var labels = {
|
|
638
|
+
network: 'Network',
|
|
639
|
+
console: 'Console',
|
|
640
|
+
navigation: 'Navigation',
|
|
641
|
+
track: 'Track',
|
|
642
|
+
zustand: 'State',
|
|
643
|
+
clipboard: 'Clipboard',
|
|
644
|
+
environment: 'Environment',
|
|
645
|
+
};
|
|
646
|
+
return labels[type] || (type ? type.charAt(0).toUpperCase() + type.slice(1) : 'Unknown');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function formatDevice(device) {
|
|
650
|
+
if (!device || typeof device !== 'object') return 'Unknown device';
|
|
651
|
+
var parts = [];
|
|
652
|
+
if (device.platform) parts.push(String(device.platform).toUpperCase());
|
|
653
|
+
if (device.model) parts.push(String(device.model));
|
|
654
|
+
if (device.osVersion) parts.push('OS ' + String(device.osVersion));
|
|
655
|
+
return parts.length ? parts.join(' / ') : 'Unknown device';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function formatIp(source) {
|
|
659
|
+
return source && source.ip ? String(source.ip) : 'unknown ip';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function statusBadge(entry) {
|
|
663
|
+
if (!entry || typeof entry !== 'object') return '<span class="badge badge-info">-</span>';
|
|
664
|
+
if (entry.error) return '<span class="badge badge-error">ERR</span>';
|
|
665
|
+
if (entry.response) {
|
|
666
|
+
var s = entry.response.status;
|
|
667
|
+
if (s >= 500) return '<span class="badge badge-error">' + s + '</span>';
|
|
668
|
+
if (s >= 400) return '<span class="badge badge-warn">' + s + '</span>';
|
|
669
|
+
if (s >= 200 && s < 300) return '<span class="badge badge-ok">' + s + '</span>';
|
|
670
|
+
return '<span class="badge badge-info">' + s + '</span>';
|
|
671
|
+
}
|
|
672
|
+
if (entry.level === 'error') return '<span class="badge badge-error">ERR</span>';
|
|
673
|
+
if (entry.level === 'warn') return '<span class="badge badge-warn">WRN</span>';
|
|
674
|
+
return '';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function summarize(entry) {
|
|
678
|
+
if (!entry || typeof entry !== 'object') return escapeHtml(String(entry));
|
|
679
|
+
if (entry.request && typeof entry.request === 'object') {
|
|
680
|
+
return escapeHtml((entry.request.method || 'GET') + ' ' + (entry.request.url || '-'));
|
|
681
|
+
}
|
|
682
|
+
if (entry.method && entry.url) return escapeHtml(entry.method + ' ' + entry.url);
|
|
683
|
+
if (entry.level && entry.data !== undefined) {
|
|
684
|
+
var data = Array.isArray(entry.data) ? entry.data.map(formatInlineValue).join(' ') : formatInlineValue(entry.data);
|
|
685
|
+
return escapeHtml(data.substring(0, 200));
|
|
686
|
+
}
|
|
687
|
+
if (entry.from || entry.to) return escapeHtml((entry.from || '-') + ' -> ' + (entry.to || '-'));
|
|
688
|
+
if (entry.eventName) return escapeHtml(String(entry.eventName));
|
|
689
|
+
if (entry.event) return escapeHtml(String(entry.event));
|
|
690
|
+
if (entry.action) return escapeHtml(String(entry.action));
|
|
691
|
+
return escapeHtml(JSON.stringify(entry).substring(0, 200));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getLogType(entry) {
|
|
695
|
+
if (!entry || typeof entry !== 'object') return 'unknown';
|
|
696
|
+
if (entry.request || entry.response || (entry.method && entry.url)) return 'network';
|
|
697
|
+
if (entry.level && entry.data !== undefined) return 'console';
|
|
698
|
+
if (entry.from || entry.to || entry.path) return 'navigation';
|
|
699
|
+
if (entry.eventName || entry.event) return 'track';
|
|
700
|
+
if (entry.prevState !== undefined || entry.nextState !== undefined || entry.storeName) return 'zustand';
|
|
701
|
+
if (entry.action) return 'zustand';
|
|
702
|
+
return 'unknown';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function toKeyPart(value) {
|
|
706
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function stringifyForKey(value) {
|
|
710
|
+
try {
|
|
711
|
+
return JSON.stringify(value).slice(0, 80);
|
|
712
|
+
} catch {
|
|
713
|
+
return String(value);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getLogEntryKey(entry, type, index) {
|
|
718
|
+
var logType = toKeyPart(type || getLogType(entry));
|
|
719
|
+
if (entry && typeof entry === 'object') {
|
|
720
|
+
if (entry.id !== undefined && entry.id !== null) {
|
|
721
|
+
return logType + '-id-' + toKeyPart(entry.id);
|
|
722
|
+
}
|
|
723
|
+
if (entry.timestamp !== undefined && entry.timestamp !== null) {
|
|
724
|
+
return logType + '-ts-' + toKeyPart(entry.timestamp) + '-' + index;
|
|
725
|
+
}
|
|
726
|
+
if (entry.request && typeof entry.request === 'object' && entry.request.url) {
|
|
727
|
+
return logType + '-req-' + toKeyPart((entry.request.method || '') + '-' + entry.request.url + '-' + index);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return logType + '-fallback-' + index + '-' + toKeyPart(stringifyForKey(entry));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseJsonString(value) {
|
|
734
|
+
if (typeof value !== 'string') return value;
|
|
735
|
+
var trimmed = value.trim();
|
|
736
|
+
if (!trimmed || !/^[\[{]/.test(trimmed)) return value;
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(trimmed);
|
|
739
|
+
} catch {
|
|
740
|
+
return value;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function formatInlineValue(value) {
|
|
745
|
+
var parsed = parseJsonString(value);
|
|
746
|
+
if (parsed === null || parsed === undefined) return String(parsed);
|
|
747
|
+
if (typeof parsed === 'string') return parsed;
|
|
748
|
+
if (typeof parsed === 'number' || typeof parsed === 'boolean') return String(parsed);
|
|
749
|
+
try {
|
|
750
|
+
return JSON.stringify(parsed);
|
|
751
|
+
} catch {
|
|
752
|
+
return String(parsed);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function highlightJson(json) {
|
|
757
|
+
var str = typeof json === 'string' ? json : JSON.stringify(json, null, 2);
|
|
758
|
+
var escaped = escapeHtml(str);
|
|
759
|
+
return escaped
|
|
760
|
+
.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
|
|
761
|
+
.replace(/: "((?:[^"\\]|\\.)*)"/g, ': <span class="json-string">"$1"</span>')
|
|
762
|
+
.replace(/: (\d+\.?\d*)/g, ': <span class="json-number">$1</span>')
|
|
763
|
+
.replace(/: (true|false)/g, ': <span class="json-bool">$1</span>')
|
|
764
|
+
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function renderValue(value) {
|
|
768
|
+
var parsed = parseJsonString(value);
|
|
769
|
+
if (parsed === null || parsed === undefined) {
|
|
770
|
+
return '<span class="value-pill">' + escapeHtml(String(parsed)) + '</span>';
|
|
771
|
+
}
|
|
772
|
+
if (typeof parsed === 'string' || typeof parsed === 'number' || typeof parsed === 'boolean') {
|
|
773
|
+
return '<span class="value-pill">' + escapeHtml(String(parsed)) + '</span>';
|
|
774
|
+
}
|
|
775
|
+
return '<pre class="json-block">' + highlightJson(parsed) + '</pre>';
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function renderRows(rows) {
|
|
779
|
+
var html = '<table class="detail-table"><tbody>';
|
|
780
|
+
rows.forEach(function(row) {
|
|
781
|
+
if (row[1] === undefined || row[1] === null || row[1] === '') return;
|
|
782
|
+
html += '<tr><th>' + escapeHtml(row[0]) + '</th><td>' + renderValue(row[1]) + '</td></tr>';
|
|
783
|
+
});
|
|
784
|
+
html += '</tbody></table>';
|
|
785
|
+
return html;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderSection(title, content, dataForCopy) {
|
|
789
|
+
var copyAttr = dataForCopy !== undefined
|
|
790
|
+
? ' data-copy="' + escapeHtml(typeof dataForCopy === 'string' ? dataForCopy : JSON.stringify(dataForCopy)) + '"'
|
|
791
|
+
: '';
|
|
792
|
+
return '<div class="detail-section"><div class="detail-section-header">' +
|
|
793
|
+
'<span class="detail-section-title">' + escapeHtml(title) + '</span>' +
|
|
794
|
+
'<button class="detail-section-copy" onclick="event.stopPropagation();copySectionData(this)" title="Copy section"' + copyAttr + '>⎘</button>' +
|
|
795
|
+
'</div><div class="detail-section-body">' + content + '</div></div>';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function renderObjectSection(title, value) {
|
|
799
|
+
var object = readObject(value);
|
|
800
|
+
if (!object) return '';
|
|
801
|
+
return renderSection(title, renderValue(object), object);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderConsoleDetails(entry) {
|
|
805
|
+
var messages = Array.isArray(entry.data) ? entry.data : [entry.data];
|
|
806
|
+
return renderSection('Console', renderRows([
|
|
807
|
+
['Level', entry.level],
|
|
808
|
+
['Time', entry.timestamp ? formatTime(new Date(entry.timestamp).toISOString()) : ''],
|
|
809
|
+
])) + renderSection('Messages',
|
|
810
|
+
'<div class="value-list">' + messages.map(renderValue).join('') + '</div>',
|
|
811
|
+
entry.data);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function renderNetworkDetails(entry) {
|
|
815
|
+
var request = readObject(entry.request) || entry;
|
|
816
|
+
var response = readObject(entry.response);
|
|
817
|
+
var method = (request.method || 'GET').toUpperCase();
|
|
818
|
+
var methodClass = 'method-' + method.toLowerCase();
|
|
819
|
+
|
|
820
|
+
var hero = '<div class="network-hero"><span class="method-badge ' + methodClass + '">' + escapeHtml(method) + '</span>' +
|
|
821
|
+
'<span class="network-url">' + escapeHtml(request.url || '-') + '</span></div>';
|
|
822
|
+
|
|
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]);
|
|
829
|
+
|
|
830
|
+
var html = renderSection('Request', hero + renderRows(reqRows), request);
|
|
831
|
+
|
|
832
|
+
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);
|
|
840
|
+
}
|
|
841
|
+
if (entry.error) {
|
|
842
|
+
html += renderSection('Error', renderValue(entry.error), entry.error);
|
|
843
|
+
}
|
|
844
|
+
return html;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function renderNavigationDetails(entry) {
|
|
848
|
+
var hero = '<div class="nav-hero">';
|
|
849
|
+
if (entry.from || entry.path) hero += '<span class="nav-from">' + escapeHtml(entry.from || entry.path || '-') + '</span>';
|
|
850
|
+
if (entry.to) hero += '<span class="nav-arrow">→</span><span class="nav-to">' + escapeHtml(entry.to) + '</span>';
|
|
851
|
+
hero += '</div>';
|
|
852
|
+
return renderSection('Navigation', hero + renderRows([
|
|
853
|
+
['Action', entry.action],
|
|
854
|
+
['Duration', entry.duration !== undefined ? entry.duration + 'ms' : ''],
|
|
855
|
+
['Time', entry.timestamp ? formatTime(new Date(entry.timestamp).toISOString()) : ''],
|
|
856
|
+
]));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function renderTrackDetails(entry) {
|
|
860
|
+
return renderSection('Event', renderRows([
|
|
861
|
+
['Name', entry.eventName || entry.event],
|
|
862
|
+
['Time', entry.timestamp ? formatTime(new Date(entry.timestamp).toISOString()) : ''],
|
|
863
|
+
])) + renderSection('Payload', renderValue(entry), entry);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function renderStateDetails(entry) {
|
|
867
|
+
return renderSection('State', renderRows([
|
|
868
|
+
['Store', entry.storeName],
|
|
869
|
+
['Action', entry.action],
|
|
870
|
+
['Time', entry.timestamp ? formatTime(new Date(entry.timestamp).toISOString()) : ''],
|
|
871
|
+
])) +
|
|
872
|
+
(entry.prevState !== undefined ? renderSection('Prev', renderValueCompact(entry.prevState), entry.prevState) : '') +
|
|
873
|
+
(entry.nextState !== undefined ? renderSection('Next', renderValueCompact(entry.nextState), entry.nextState) : '');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function renderValueCompact(value) {
|
|
877
|
+
var parsed = parseJsonString(value);
|
|
878
|
+
if (parsed === null || parsed === undefined) {
|
|
879
|
+
return '<span class="value-pill">' + escapeHtml(String(parsed)) + '</span>';
|
|
880
|
+
}
|
|
881
|
+
if (typeof parsed === 'string' || typeof parsed === 'number' || typeof parsed === 'boolean') {
|
|
882
|
+
return '<span class="value-pill">' + escapeHtml(String(parsed)) + '</span>';
|
|
883
|
+
}
|
|
884
|
+
return '<pre class="json-block json-compact">' + highlightJson(parsed) + '</pre>';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function renderLogDetails(entry, type) {
|
|
888
|
+
var logType = type || getLogType(entry);
|
|
889
|
+
if (!entry || typeof entry !== 'object') {
|
|
890
|
+
return renderSection('Value', renderValue(entry));
|
|
891
|
+
}
|
|
892
|
+
if (logType === 'network') return renderNetworkDetails(entry);
|
|
893
|
+
if (logType === 'console') return renderConsoleDetails(entry);
|
|
894
|
+
if (logType === 'navigation') return renderNavigationDetails(entry);
|
|
895
|
+
if (logType === 'track') return renderTrackDetails(entry);
|
|
896
|
+
if (logType === 'zustand') return renderStateDetails(entry);
|
|
897
|
+
return renderObjectSection(labelForType(logType), entry) || renderSection('Raw', renderValue(entry));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// --- Search ---
|
|
901
|
+
|
|
902
|
+
function matchSearch(text) {
|
|
903
|
+
if (!searchTerm) return text;
|
|
904
|
+
var escaped = escapeHtml(text);
|
|
905
|
+
var re = new RegExp('(' + escapeRegex(searchTerm) + ')', 'gi');
|
|
906
|
+
return escaped.replace(re, '<mark>$1</mark>');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function escapeRegex(s) {
|
|
910
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function entryMatchesSearch(entry) {
|
|
914
|
+
if (!searchTerm) return true;
|
|
915
|
+
var term = searchTerm.toLowerCase();
|
|
916
|
+
var text = summarize(entry);
|
|
917
|
+
// Remove HTML for matching
|
|
918
|
+
var div = document.createElement('div');
|
|
919
|
+
div.innerHTML = text;
|
|
920
|
+
return div.textContent.toLowerCase().indexOf(term) >= 0;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// --- Views ---
|
|
924
|
+
|
|
925
|
+
function renderList() {
|
|
926
|
+
currentDevice = null;
|
|
927
|
+
expandedRows = {};
|
|
928
|
+
searchTerm = '';
|
|
929
|
+
focusedIndex = -1;
|
|
930
|
+
pulseDot.style.background = 'var(--text3)';
|
|
931
|
+
pulseDot.style.boxShadow = 'none';
|
|
932
|
+
statusEl.textContent = 'fetching...';
|
|
933
|
+
api('/devices').then(function(data) {
|
|
934
|
+
statusEl.textContent = '';
|
|
935
|
+
pulseDot.style.background = 'var(--cyan)';
|
|
936
|
+
pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
|
|
937
|
+
var devices = data.devices || [];
|
|
938
|
+
if (!devices.length) {
|
|
939
|
+
app.innerHTML =
|
|
940
|
+
'<div class="empty">' +
|
|
941
|
+
'<div class="empty-icon">_</div>' +
|
|
942
|
+
'<p>No device logs received yet.</p>' +
|
|
943
|
+
'<p style="margin-top:12px;font-size:13px">POST a report to <code>/report</code> to see data here.</p>' +
|
|
944
|
+
'</div>' +
|
|
945
|
+
renderCurlPanel('Curl quick read', [
|
|
946
|
+
curlCommand('/health'),
|
|
947
|
+
curlCommand('/devices'),
|
|
948
|
+
curlCommand('/devices/latest'),
|
|
949
|
+
]);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
var html = '<div class="section-title">Devices <span style="color:var(--text3);font-weight:400">(' + devices.length + ')</span></div>';
|
|
953
|
+
html += renderCurlPanel('Curl quick read', [
|
|
954
|
+
curlCommand('/health'),
|
|
955
|
+
curlCommand('/devices'),
|
|
956
|
+
curlCommand('/devices/latest'),
|
|
957
|
+
]);
|
|
958
|
+
html += '<div class="device-grid">';
|
|
959
|
+
devices.forEach(function(deviceLog, i) {
|
|
960
|
+
var lc = deviceLog.logCount || {};
|
|
961
|
+
var deviceText = formatDevice(deviceLog.device);
|
|
962
|
+
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) + '\'">';
|
|
964
|
+
html += '<div><div class="device-title">' + escapeHtml(deviceText) + '</div>';
|
|
965
|
+
html += '<div class="device-subtitle">IP ' + escapeHtml(ipText) + '</div></div>';
|
|
966
|
+
html += '<div class="device-meta-group">';
|
|
967
|
+
html += '<div class="device-meta-line"><strong>Device</strong>' + escapeHtml(deviceLog.deviceId) + '</div>';
|
|
968
|
+
html += '<div class="device-meta-line"><strong>Last seen</strong>' + formatTime(deviceLog.lastSeenAt || deviceLog.receivedAt) + '</div>';
|
|
969
|
+
html += '</div>';
|
|
970
|
+
html += '<div class="device-tags">' + renderDeviceTags(lc) + '</div>';
|
|
971
|
+
html += '<div class="device-arrow">›</div>';
|
|
972
|
+
html += '</div>';
|
|
973
|
+
});
|
|
974
|
+
html += '</div>';
|
|
975
|
+
app.innerHTML = html;
|
|
976
|
+
}).catch(function(err) {
|
|
977
|
+
statusEl.textContent = '';
|
|
978
|
+
pulseDot.style.background = 'var(--red)';
|
|
979
|
+
pulseDot.style.boxShadow = '0 0 8px var(--red)';
|
|
980
|
+
app.innerHTML = '<div class="empty" style="color:var(--red)">Connection failed: ' + escapeHtml(err.message) + '</div>';
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function renderDetail(deviceId) {
|
|
985
|
+
expandedRows = {};
|
|
986
|
+
searchTerm = '';
|
|
987
|
+
focusedIndex = -1;
|
|
988
|
+
window._currentFilterType = '';
|
|
989
|
+
window._failedOnly = false;
|
|
990
|
+
statusEl.textContent = 'loading...';
|
|
991
|
+
api('/devices/' + encodeURIComponent(deviceId)).then(function(data) {
|
|
992
|
+
statusEl.textContent = '';
|
|
993
|
+
pulseDot.style.background = 'var(--cyan)';
|
|
994
|
+
pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
|
|
995
|
+
currentDevice = data;
|
|
996
|
+
var report = data.report || {};
|
|
997
|
+
var logs = report.logs || {};
|
|
998
|
+
var logTypes = Object.keys(logs);
|
|
999
|
+
|
|
1000
|
+
var html = '';
|
|
1001
|
+
|
|
1002
|
+
// Back link
|
|
1003
|
+
html += '<a href="#" class="back-link" onclick="location.hash=\'\';return false">';
|
|
1004
|
+
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
|
+
html += 'All devices</a>';
|
|
1006
|
+
|
|
1007
|
+
// Device header card
|
|
1008
|
+
html += '<div class="detail-header">';
|
|
1009
|
+
html += '<div class="detail-id">' + escapeHtml(data.deviceId) + '</div>';
|
|
1010
|
+
html += '<div class="detail-meta">';
|
|
1011
|
+
html += '<span class="detail-meta-item"><strong>Last seen</strong> ' + formatTime(data.lastSeenAt || data.receivedAt) + '</span>';
|
|
1012
|
+
var totalLogs = Object.values(data.logCount || {}).reduce(function(a, b) { return a + b; }, 0);
|
|
1013
|
+
html += '<span class="detail-meta-item" data-type="Entries"><strong>Entries</strong> ' + totalLogs + '</span>';
|
|
1014
|
+
if (data.source && data.source.ip) {
|
|
1015
|
+
html += '<span class="detail-meta-item"><strong>IP</strong> ' + escapeHtml(data.source.ip) + '</span>';
|
|
1016
|
+
}
|
|
1017
|
+
Object.entries(data.logCount || {}).forEach(function(e) {
|
|
1018
|
+
html += '<span class="detail-meta-item" data-type="' + e[0] + '"><strong>' + escapeHtml(labelForType(e[0])) + '</strong> ' + e[1] + '</span>';
|
|
1019
|
+
});
|
|
1020
|
+
html += '</div>';
|
|
1021
|
+
|
|
1022
|
+
// Device info
|
|
1023
|
+
var device = report.device;
|
|
1024
|
+
if (device && typeof device === 'object') {
|
|
1025
|
+
html += '<div class="device-info">';
|
|
1026
|
+
var pClass = device.platform === 'ios' ? 'platform-ios' : device.platform === 'android' ? 'platform-android' : '';
|
|
1027
|
+
html += '<span class="device-badge ' + pClass + '">' + escapeHtml((device.platform || 'unknown').toUpperCase()) + '</span>';
|
|
1028
|
+
if (device.model) html += '<span class="device-badge">' + escapeHtml(device.model) + '</span>';
|
|
1029
|
+
if (device.osVersion) html += '<span class="device-badge">OS ' + escapeHtml(device.osVersion) + '</span>';
|
|
1030
|
+
if (device.appVersion) html += '<span class="device-badge">v' + escapeHtml(device.appVersion) + '</span>';
|
|
1031
|
+
if (sseConnected) html += '<span class="live-badge"><span class="live-badge-dot"></span>LIVE</span>';
|
|
1032
|
+
html += '</div>';
|
|
1033
|
+
} else if (sseConnected) {
|
|
1034
|
+
html += '<div class="device-info"><span class="live-badge"><span class="live-badge-dot"></span>LIVE</span></div>';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
html += '</div>';
|
|
1038
|
+
|
|
1039
|
+
html += renderCurlPanel('Curl this device', [
|
|
1040
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId)),
|
|
1041
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?limit=100'),
|
|
1042
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?type=network&failedOnly=true&limit=50'),
|
|
1043
|
+
curlCommand('/devices/' + encodeURIComponent(deviceId) + '/logs?type=console&limit=100'),
|
|
1044
|
+
]);
|
|
1045
|
+
|
|
1046
|
+
// Tabs
|
|
1047
|
+
html += '<div class="tabs">';
|
|
1048
|
+
html += '<button class="tab active" data-type="" onclick="filterType(this,\'\')">All<span class="count">' + totalLogs + '</span></button>';
|
|
1049
|
+
logTypes.forEach(function(t) {
|
|
1050
|
+
var count = logs[t] ? logs[t].length : 0;
|
|
1051
|
+
html += '<button class="tab" data-type="' + t + '" onclick="filterType(this,\'' + t + '\')">' + escapeHtml(labelForType(t)) + '<span class="count">' + count + '</span></button>';
|
|
1052
|
+
});
|
|
1053
|
+
html += '</div>';
|
|
1054
|
+
|
|
1055
|
+
// Toolbar with search
|
|
1056
|
+
html += '<div class="toolbar">';
|
|
1057
|
+
html += '<div class="search-wrap">';
|
|
1058
|
+
html += '<span class="search-icon">⚲</span>';
|
|
1059
|
+
html += '<input class="search-input" id="searchInput" type="text" placeholder="Search logs..." autocomplete="off">';
|
|
1060
|
+
html += '<button class="search-clear" id="searchClear" onclick="clearSearch()">×</button>';
|
|
1061
|
+
html += '</div>';
|
|
1062
|
+
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>';
|
|
1064
|
+
html += '</div>';
|
|
1065
|
+
|
|
1066
|
+
// Log container
|
|
1067
|
+
html += '<div id="logsContainer"></div>';
|
|
1068
|
+
|
|
1069
|
+
// Actions
|
|
1070
|
+
html += '<div class="actions">';
|
|
1071
|
+
html += '<button class="btn" onclick="copyJSON()">';
|
|
1072
|
+
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>';
|
|
1073
|
+
html += 'Copy JSON</button>';
|
|
1074
|
+
html += '<span style="flex:1"></span>';
|
|
1075
|
+
html += '<span style="font-size:10px;color:var(--text3);font-family:var(--font-mono)"><span class="kbd">/</span> search <span class="kbd">j</span><span class="kbd">k</span> navigate <span class="kbd">Enter</span> expand <span class="kbd">Esc</span> back</span>';
|
|
1076
|
+
html += '</div>';
|
|
1077
|
+
|
|
1078
|
+
app.innerHTML = html;
|
|
1079
|
+
|
|
1080
|
+
// Wire up search
|
|
1081
|
+
var searchInput = document.getElementById('searchInput');
|
|
1082
|
+
searchInput.addEventListener('input', function() {
|
|
1083
|
+
searchTerm = this.value.trim();
|
|
1084
|
+
var clearBtn = document.getElementById('searchClear');
|
|
1085
|
+
if (clearBtn) clearBtn.classList.toggle('visible', searchTerm.length > 0);
|
|
1086
|
+
applyFilters();
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
document.getElementById('limitInput').addEventListener('change', applyFilters);
|
|
1090
|
+
renderLogs(logs, '', 50, false);
|
|
1091
|
+
}).catch(function(err) {
|
|
1092
|
+
statusEl.textContent = '';
|
|
1093
|
+
app.innerHTML = '<div class="empty" style="color:var(--red)">Failed to load: ' + escapeHtml(err.message) + '</div>';
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function renderLogs(logs, type, limit, failedOnly) {
|
|
1098
|
+
var entries = [];
|
|
1099
|
+
if (type && logs[type]) {
|
|
1100
|
+
entries = Array.isArray(logs[type])
|
|
1101
|
+
? logs[type].map(function(entry, index) { return { type: type, entry: entry, order: index }; })
|
|
1102
|
+
: [];
|
|
1103
|
+
} else {
|
|
1104
|
+
Object.entries(logs).forEach(function(logGroup) {
|
|
1105
|
+
var logType = logGroup[0];
|
|
1106
|
+
var value = logGroup[1];
|
|
1107
|
+
if (Array.isArray(value)) {
|
|
1108
|
+
value.forEach(function(entry) {
|
|
1109
|
+
entries.push({ type: logType, entry: entry, order: entries.length });
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (failedOnly) {
|
|
1116
|
+
entries = entries.filter(function(item) { return isFailedEntry(item.entry); });
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Search filter
|
|
1120
|
+
if (searchTerm) {
|
|
1121
|
+
entries = entries.filter(function(item) { return entryMatchesSearch(item.entry); });
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
entries = entries
|
|
1125
|
+
.slice()
|
|
1126
|
+
.sort(function(a, b) {
|
|
1127
|
+
var byTime = readTimestamp(b.entry) - readTimestamp(a.entry);
|
|
1128
|
+
return byTime || b.order - a.order;
|
|
1129
|
+
})
|
|
1130
|
+
.slice(0, limit);
|
|
1131
|
+
|
|
1132
|
+
var container = document.getElementById('logsContainer');
|
|
1133
|
+
if (!entries.length) {
|
|
1134
|
+
container.innerHTML = '<div class="empty" style="padding:40px"><div class="empty-icon" style="font-size:24px">0</div><p style="font-size:13px">' +
|
|
1135
|
+
(searchTerm ? 'No logs match "' + escapeHtml(searchTerm) + '"' : 'No logs match filters.') +
|
|
1136
|
+
'</p></div>';
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
focusedIndex = -1;
|
|
1141
|
+
var html = '<div class="log-list">';
|
|
1142
|
+
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">⎘</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>';
|
|
1174
|
+
});
|
|
1175
|
+
html += '</div>';
|
|
1176
|
+
container.innerHTML = html;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// --- Global handlers ---
|
|
1180
|
+
|
|
1181
|
+
function readVisibleLogOptions() {
|
|
1182
|
+
return {
|
|
1183
|
+
type: window._currentFilterType || '',
|
|
1184
|
+
failedOnly: window._failedOnly || false,
|
|
1185
|
+
limit: document.getElementById('limitInput') ? parseInt(document.getElementById('limitInput').value, 10) || 50 : 50,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function rerenderVisibleLogs() {
|
|
1190
|
+
if (!currentDevice) return;
|
|
1191
|
+
var logs = currentDevice.report ? currentDevice.report.logs : {};
|
|
1192
|
+
var options = readVisibleLogOptions();
|
|
1193
|
+
renderLogs(logs, options.type, options.limit, options.failedOnly);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function refreshCurrentDevice() {
|
|
1197
|
+
if (!currentDevice) {
|
|
1198
|
+
renderList();
|
|
1199
|
+
return Promise.resolve();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
statusEl.textContent = 'refreshing...';
|
|
1203
|
+
return api('/devices/' + encodeURIComponent(currentDevice.deviceId)).then(function(data) {
|
|
1204
|
+
statusEl.textContent = '';
|
|
1205
|
+
if (!data) return;
|
|
1206
|
+
currentDevice.report = data.report;
|
|
1207
|
+
currentDevice.logCount = data.logCount;
|
|
1208
|
+
currentDevice.receivedAt = data.receivedAt;
|
|
1209
|
+
currentDevice.lastSeenAt = data.lastSeenAt;
|
|
1210
|
+
rerenderVisibleLogs();
|
|
1211
|
+
updateTabCounts();
|
|
1212
|
+
}).catch(function(err) {
|
|
1213
|
+
statusEl.textContent = '';
|
|
1214
|
+
showToast('Refresh failed: ' + err.message);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function refreshCurrentView() {
|
|
1219
|
+
return refreshCurrentDevice();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
window.refresh = function() { refreshCurrentView(); };
|
|
1223
|
+
|
|
1224
|
+
window.filterType = function(btn, type) {
|
|
1225
|
+
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
1226
|
+
btn.classList.add('active');
|
|
1227
|
+
window._currentFilterType = type;
|
|
1228
|
+
applyFilters();
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
window.toggleFailed = function() {
|
|
1232
|
+
var el = document.getElementById('failedToggle');
|
|
1233
|
+
window._failedOnly = !window._failedOnly;
|
|
1234
|
+
if (window._failedOnly) { el.classList.add('on'); } else { el.classList.remove('on'); }
|
|
1235
|
+
applyFilters();
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
window.applyFilters = function() {
|
|
1239
|
+
if (!currentDevice) return;
|
|
1240
|
+
expandedRows = {};
|
|
1241
|
+
rerenderVisibleLogs();
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
window.clearSearch = function() {
|
|
1245
|
+
var input = document.getElementById('searchInput');
|
|
1246
|
+
if (input) input.value = '';
|
|
1247
|
+
searchTerm = '';
|
|
1248
|
+
var clearBtn = document.getElementById('searchClear');
|
|
1249
|
+
if (clearBtn) clearBtn.classList.remove('visible');
|
|
1250
|
+
applyFilters();
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
window.toggleRow = function(rowId) {
|
|
1254
|
+
var entry = document.getElementById('entry-' + rowId);
|
|
1255
|
+
var detail = document.getElementById('detail-' + rowId);
|
|
1256
|
+
if (!entry || !detail) return;
|
|
1257
|
+
expandedRows[rowId] = !expandedRows[rowId];
|
|
1258
|
+
if (expandedRows[rowId]) {
|
|
1259
|
+
entry.classList.add('expanded');
|
|
1260
|
+
} else {
|
|
1261
|
+
entry.classList.remove('expanded');
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
window.toggleCurl = function(id) {
|
|
1266
|
+
var body = document.getElementById(id);
|
|
1267
|
+
var toggle = document.getElementById('toggle-' + id);
|
|
1268
|
+
if (!body) return;
|
|
1269
|
+
body.classList.toggle('open');
|
|
1270
|
+
if (toggle) toggle.classList.toggle('open');
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
window.copyJSON = function() {
|
|
1274
|
+
if (!currentDevice) return;
|
|
1275
|
+
var text = JSON.stringify(currentDevice.report, null, 2);
|
|
1276
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1277
|
+
showToast('Copied to clipboard');
|
|
1278
|
+
}).catch(function() {
|
|
1279
|
+
var ta = document.createElement('textarea');
|
|
1280
|
+
ta.value = text;
|
|
1281
|
+
ta.style.position = 'fixed';
|
|
1282
|
+
ta.style.opacity = '0';
|
|
1283
|
+
document.body.appendChild(ta);
|
|
1284
|
+
ta.select();
|
|
1285
|
+
document.execCommand('copy');
|
|
1286
|
+
document.body.removeChild(ta);
|
|
1287
|
+
showToast('Copied to clipboard');
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
window.copyEntryJSON = function(rowId) {
|
|
1292
|
+
// Find the entry data from current device logs
|
|
1293
|
+
if (!currentDevice) return;
|
|
1294
|
+
var el = document.getElementById('entry-' + rowId);
|
|
1295
|
+
if (!el) return;
|
|
1296
|
+
|
|
1297
|
+
// Re-derive the entry from current logs
|
|
1298
|
+
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
|
+
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);
|
|
1324
|
+
|
|
1325
|
+
var idx = parseInt(el.getAttribute('data-index'), 10);
|
|
1326
|
+
if (isNaN(idx) || idx < 0 || idx >= entries.length) return;
|
|
1327
|
+
var text = JSON.stringify(entries[idx].entry, null, 2);
|
|
1328
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1329
|
+
showToast('Entry copied');
|
|
1330
|
+
}).catch(function() {
|
|
1331
|
+
var ta = document.createElement('textarea');
|
|
1332
|
+
ta.value = text;
|
|
1333
|
+
ta.style.position = 'fixed';
|
|
1334
|
+
ta.style.opacity = '0';
|
|
1335
|
+
document.body.appendChild(ta);
|
|
1336
|
+
ta.select();
|
|
1337
|
+
document.execCommand('copy');
|
|
1338
|
+
document.body.removeChild(ta);
|
|
1339
|
+
showToast('Entry copied');
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
window.copySectionData = function(btn) {
|
|
1344
|
+
var data = btn.getAttribute('data-copy');
|
|
1345
|
+
if (!data) return;
|
|
1346
|
+
navigator.clipboard.writeText(data).then(function() {
|
|
1347
|
+
showToast('Section copied');
|
|
1348
|
+
}).catch(function() {
|
|
1349
|
+
var ta = document.createElement('textarea');
|
|
1350
|
+
ta.value = data;
|
|
1351
|
+
ta.style.position = 'fixed';
|
|
1352
|
+
ta.style.opacity = '0';
|
|
1353
|
+
document.body.appendChild(ta);
|
|
1354
|
+
ta.select();
|
|
1355
|
+
document.execCommand('copy');
|
|
1356
|
+
document.body.removeChild(ta);
|
|
1357
|
+
showToast('Section copied');
|
|
1358
|
+
});
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
// --- Keyboard shortcuts ---
|
|
1362
|
+
|
|
1363
|
+
document.addEventListener('keydown', function(e) {
|
|
1364
|
+
// Don't intercept when typing in inputs
|
|
1365
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
1366
|
+
if (e.key === 'Escape') {
|
|
1367
|
+
e.target.blur();
|
|
1368
|
+
if (e.target.id === 'searchInput' && searchTerm) {
|
|
1369
|
+
clearSearch();
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (e.key === '/') {
|
|
1376
|
+
e.preventDefault();
|
|
1377
|
+
var searchInput = document.getElementById('searchInput');
|
|
1378
|
+
if (searchInput) searchInput.focus();
|
|
1379
|
+
} else if (e.key === 'Escape') {
|
|
1380
|
+
if (currentDevice) {
|
|
1381
|
+
location.hash = '';
|
|
1382
|
+
}
|
|
1383
|
+
} else if (e.key === 'j' || e.key === 'k') {
|
|
1384
|
+
e.preventDefault();
|
|
1385
|
+
navigateEntries(e.key === 'j' ? 1 : -1);
|
|
1386
|
+
} else if (e.key === 'Enter') {
|
|
1387
|
+
e.preventDefault();
|
|
1388
|
+
toggleFocusedEntry();
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
function navigateEntries(direction) {
|
|
1393
|
+
var list = document.querySelector('.log-list');
|
|
1394
|
+
if (!list) return;
|
|
1395
|
+
var items = list.querySelectorAll('.log-entry');
|
|
1396
|
+
if (!items.length) return;
|
|
1397
|
+
|
|
1398
|
+
// Remove previous focus
|
|
1399
|
+
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
1400
|
+
items[focusedIndex].classList.remove('focused');
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
focusedIndex += direction;
|
|
1404
|
+
if (focusedIndex < 0) focusedIndex = 0;
|
|
1405
|
+
if (focusedIndex >= items.length) focusedIndex = items.length - 1;
|
|
1406
|
+
|
|
1407
|
+
items[focusedIndex].classList.add('focused');
|
|
1408
|
+
items[focusedIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function toggleFocusedEntry() {
|
|
1412
|
+
if (focusedIndex < 0) return;
|
|
1413
|
+
var list = document.querySelector('.log-list');
|
|
1414
|
+
if (!list) return;
|
|
1415
|
+
var items = list.querySelectorAll('.log-entry');
|
|
1416
|
+
if (focusedIndex >= items.length) return;
|
|
1417
|
+
items[focusedIndex].click();
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// --- Routing ---
|
|
1421
|
+
|
|
1422
|
+
window._currentFilterType = '';
|
|
1423
|
+
window._failedOnly = false;
|
|
1424
|
+
|
|
1425
|
+
function route() {
|
|
1426
|
+
var hash = location.hash.replace('#', '');
|
|
1427
|
+
if (hash.startsWith('device/')) {
|
|
1428
|
+
renderDetail(decodeURIComponent(hash.substring(7)));
|
|
1429
|
+
} else {
|
|
1430
|
+
renderList();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// --- Incremental DOM updates ---
|
|
1435
|
+
|
|
1436
|
+
function isFailedEntry(e) {
|
|
1437
|
+
return e && typeof e === 'object' && (
|
|
1438
|
+
Boolean(e.error) ||
|
|
1439
|
+
e.level === 'error' ||
|
|
1440
|
+
(e.response && (e.response.success === false || e.response.status >= 400))
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function renderDeviceTags(logCount) {
|
|
1445
|
+
var html = '';
|
|
1446
|
+
Object.entries(logCount || {}).forEach(function(e) {
|
|
1447
|
+
var type = String(e[0]);
|
|
1448
|
+
html += '<span class="tag tag-' + toKeyPart(type) + '">' + escapeHtml(labelForType(type)) + ' ' + escapeHtml(String(e[1])) + '</span>';
|
|
1449
|
+
});
|
|
1450
|
+
return html;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function appendDeviceCard(payload) {
|
|
1454
|
+
var grid = document.querySelector('.device-grid');
|
|
1455
|
+
if (!grid) { renderList(); return; }
|
|
1456
|
+
|
|
1457
|
+
var deviceId = payload.deviceId;
|
|
1458
|
+
if (!deviceId) return;
|
|
1459
|
+
var existing = grid.querySelector('[data-device-id="' + CSS.escape(deviceId) + '"]');
|
|
1460
|
+
if (existing) {
|
|
1461
|
+
var tags = existing.querySelector('.device-tags');
|
|
1462
|
+
if (tags) tags.innerHTML = renderDeviceTags(payload.logCount || {});
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
var lc = payload.logCount || {};
|
|
1467
|
+
var deviceText = formatDevice(payload.device);
|
|
1468
|
+
var ipText = formatIp(payload.source);
|
|
1469
|
+
var card = document.createElement('div');
|
|
1470
|
+
card.className = 'device-card';
|
|
1471
|
+
card.setAttribute('data-device-id', deviceId);
|
|
1472
|
+
card.setAttribute('onclick', "location.hash='device/" + encodeURIComponent(deviceId) + "'");
|
|
1473
|
+
var html = '<div><div class="device-title">' + escapeHtml(deviceText) + '</div>';
|
|
1474
|
+
html += '<div class="device-subtitle">IP ' + escapeHtml(ipText) + '</div></div>';
|
|
1475
|
+
html += '<div class="device-meta-group">';
|
|
1476
|
+
html += '<div class="device-meta-line"><strong>Device</strong>' + escapeHtml(deviceId) + '</div>';
|
|
1477
|
+
html += '<div class="device-meta-line"><strong>Last seen</strong> just now</div>';
|
|
1478
|
+
html += '</div>';
|
|
1479
|
+
html += '<div class="device-tags">' + renderDeviceTags(lc) + '</div>';
|
|
1480
|
+
html += '<div class="device-arrow">›</div>';
|
|
1481
|
+
card.innerHTML = html;
|
|
1482
|
+
grid.prepend(card);
|
|
1483
|
+
|
|
1484
|
+
var titleEl = document.querySelector('.section-title');
|
|
1485
|
+
if (titleEl) {
|
|
1486
|
+
var count = grid.querySelectorAll('.device-card').length;
|
|
1487
|
+
titleEl.innerHTML = 'Devices <span style="color:var(--text3);font-weight:400">(' + count + ')</span>';
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
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
|
+
function updateTabCounts() {
|
|
1569
|
+
if (!currentDevice) return;
|
|
1570
|
+
var logs = currentDevice.report ? currentDevice.report.logs : {};
|
|
1571
|
+
var totalLogs = Object.values(logs).reduce(function(a, v) { return a + (Array.isArray(v) ? v.length : 0); }, 0);
|
|
1572
|
+
|
|
1573
|
+
document.querySelectorAll('.tab').forEach(function(tab) {
|
|
1574
|
+
var t = tab.getAttribute('data-type') || '';
|
|
1575
|
+
var countEl = tab.querySelector('.count');
|
|
1576
|
+
var c = t ? (logs[t] ? logs[t].length : 0) : totalLogs;
|
|
1577
|
+
if (countEl) countEl.textContent = c;
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
document.querySelectorAll('.detail-meta-item[data-type]').forEach(function(el) {
|
|
1581
|
+
var t = el.getAttribute('data-type');
|
|
1582
|
+
var strong = el.querySelector('strong');
|
|
1583
|
+
if (!strong) return;
|
|
1584
|
+
var c = t === 'Entries' ? totalLogs : (logs[t] ? logs[t].length : 0);
|
|
1585
|
+
el.innerHTML = '<strong>' + strong.textContent + '</strong> ' + c;
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// --- SSE ---
|
|
1590
|
+
|
|
1591
|
+
var sseConnected = false;
|
|
1592
|
+
var eventSource = null;
|
|
1593
|
+
|
|
1594
|
+
function connectSSE() {
|
|
1595
|
+
if (eventSource) { try { eventSource.close(); } catch {} }
|
|
1596
|
+
eventSource = new EventSource(withAuth('/events'));
|
|
1597
|
+
|
|
1598
|
+
eventSource.addEventListener('logs', function(e) {
|
|
1599
|
+
try {
|
|
1600
|
+
var payload = JSON.parse(e.data);
|
|
1601
|
+
sseConnected = true;
|
|
1602
|
+
pulseDot.style.background = 'var(--cyan)';
|
|
1603
|
+
pulseDot.style.boxShadow = '0 0 8px var(--cyan),0 0 20px rgba(0,229,255,.3)';
|
|
1604
|
+
|
|
1605
|
+
if (!currentDevice) {
|
|
1606
|
+
appendDeviceCard(payload);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (payload.deviceId === currentDevice.deviceId) {
|
|
1611
|
+
if (payload.type === 'delta' && payload.delta) {
|
|
1612
|
+
var deltaLogs = payload.delta.logs || {};
|
|
1613
|
+
var report = currentDevice.report || { version: 2, logs: {} };
|
|
1614
|
+
if (!report.logs) report.logs = {};
|
|
1615
|
+
|
|
1616
|
+
Object.entries(deltaLogs).forEach(function(entry) {
|
|
1617
|
+
var type = entry[0];
|
|
1618
|
+
var entries = entry[1];
|
|
1619
|
+
if (!Array.isArray(entries)) return;
|
|
1620
|
+
if (!report.logs[type]) report.logs[type] = [];
|
|
1621
|
+
report.logs[type] = report.logs[type].concat(entries);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
currentDevice.report = report;
|
|
1625
|
+
if (payload.logCount) currentDevice.logCount = payload.logCount;
|
|
1626
|
+
appendDeltaLogs(deltaLogs);
|
|
1627
|
+
updateTabCounts();
|
|
1628
|
+
} else if (payload.type === 'full') {
|
|
1629
|
+
refreshCurrentDevice();
|
|
1630
|
+
}
|
|
1631
|
+
} else if (!location.hash.startsWith('device/')) {
|
|
1632
|
+
appendDeviceCard(payload);
|
|
1633
|
+
}
|
|
1634
|
+
} catch {}
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
eventSource.onerror = function() {
|
|
1638
|
+
sseConnected = false;
|
|
1639
|
+
pulseDot.style.background = 'var(--amber)';
|
|
1640
|
+
pulseDot.style.boxShadow = '0 0 8px var(--amber)';
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
window.addEventListener('hashchange', route);
|
|
1645
|
+
connectSSE();
|
|
1646
|
+
route();
|
|
1647
|
+
|
|
1648
|
+
// Fetch LAN IPs from /health and display in header
|
|
1649
|
+
api('/health').then(function(data) {
|
|
1650
|
+
var ips = data && data.ips;
|
|
1651
|
+
if (Array.isArray(ips) && ips.length) {
|
|
1652
|
+
var ipEl = document.getElementById('ipHint');
|
|
1653
|
+
if (ipEl) {
|
|
1654
|
+
ipEl.textContent = 'LAN ' + ips.join(' / ');
|
|
1655
|
+
ipEl.style.display = '';
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}).catch(function() {});
|
|
1659
|
+
})();
|
|
1660
|
+
</script>
|
|
1661
|
+
</body>
|
|
1662
|
+
</html>
|