12
12
flex-direction : column;
13
13
font-family : "Courier New" , Courier, monospace;
14
14
}
15
- # log-controls {
16
- margin : 0.5em ;
15
+ .log-container {
17
16
display : flex;
18
- align-items : center;
19
- justify-content : space-between; /* Spaces out elements evenly */
17
+ flex : 1 ;
18
+ gap : 0.5em ;
19
+ margin : 0.5em ;
20
+ min-height : 0 ;
20
21
}
21
- # log-controls input {
22
+ .log-column {
23
+ display : flex;
24
+ flex-direction : column;
22
25
flex : 1 ;
26
+ min-width : 0 ;
27
+ transition : flex 0.3s ease;
28
+ }
29
+ .log-column .minimized {
30
+ flex : 0.1 ;
31
+ max-width : 50px ;
32
+ border : 1px solid # 777 ;
33
+ color : green;
23
34
}
24
- # log-controls input : focus {
25
- outline : none; /* Ensures no outline is shown when the input is focused */
35
+ .log-controls {
36
+ display : grid;
37
+ grid-template-columns : 1fr auto;
38
+ gap : 0.5em ;
39
+ margin-bottom : 0.5em ;
26
40
}
27
- # log-stream {
41
+ .log-controls input {
42
+ width : 100% ;
43
+ padding : 4px ;
44
+ }
45
+ .log-controls input : focus {
46
+ outline : none;
47
+ }
48
+ .log-stream {
28
49
flex : 1 ;
29
- margin : 0.5em ;
30
50
padding : 1em ;
31
51
background : # f4f4f4 ;
32
52
overflow-y : auto;
33
- white-space : pre-wrap; /* Ensures line wrapping */
34
- word-wrap : break-word; /* Ensures long words wrap */
53
+ white-space : pre-wrap;
54
+ word-wrap : break-word;
55
+ min-height : 0 ;
35
56
}
36
57
37
58
.regex-error {
38
59
background-color : # ff0000 !important ;
39
60
}
40
61
62
+ /* Make headers clickable and show pointer cursor */
63
+ h2 {
64
+ cursor : pointer;
65
+ user-select : none;
66
+ margin : 0 0 0.5em 0 ;
67
+ padding : 0.5em ;
68
+ }
69
+
70
+ h2 : hover {
71
+ background-color : rgba (0 , 0 , 0 , 0.05 );
72
+ }
73
+
41
74
/* Dark mode styles */
42
75
@media (prefers-color-scheme : dark) {
43
76
body {
44
77
background-color : # 333 ;
45
78
color : # fff ;
46
79
}
47
80
48
- # log-stream {
81
+ . log-stream {
49
82
background : # 444 ;
50
83
color : # fff ;
51
84
}
52
85
53
- # log-controls input {
86
+ . log-controls input {
54
87
background : # 555 ;
55
88
color : # fff ;
56
89
border : 1px solid # 777 ;
57
90
}
58
91
59
- # log-controls button {
92
+ . log-controls button {
60
93
background : # 555 ;
61
94
color : # fff ;
62
95
border : 1px solid # 777 ;
63
96
}
97
+
98
+ h2 : hover {
99
+ background-color : rgba (255 , 255 , 255 , 0.1 );
100
+ }
101
+ }
102
+
103
+ /* Hide content when minimized */
104
+ .log-column .minimized .log-controls ,
105
+ .log-column .minimized .log-stream {
106
+ display : none;
107
+ }
108
+
109
+ .log-column .minimized h2 {
110
+ writing-mode : vertical-rl;
111
+ text-orientation : mixed;
112
+ transform : rotate (180deg );
113
+ white-space : nowrap;
114
+ margin : auto;
64
115
}
65
116
</ style >
66
117
</ head >
67
118
< body >
68
- < pre id ="log-stream "> Waiting for logs...</ pre >
69
- < div id ="log-controls ">
70
- < input type ="text " id ="filter-input " placeholder ="regex filter ">
71
- < button id ="clear-button "> clear</ button >
119
+ < div class ="log-container ">
120
+ < div class ="log-column ">
121
+ < h2 > Proxy Logs</ h2 >
122
+ < div class ="log-controls ">
123
+ < input type ="text " id ="proxy-filter-input " placeholder ="proxy regex filter ">
124
+ < button id ="proxy-clear-button "> clear</ button >
125
+ </ div >
126
+ < pre class ="log-stream " id ="proxy-log-stream "> Waiting for proxy logs...</ pre >
127
+ </ div >
128
+ < div class ="log-column minimized ">
129
+ < h2 > Upstream Logs</ h2 >
130
+ < div class ="log-controls ">
131
+ < input type ="text " id ="upstream-filter-input " placeholder ="upstream regex filter ">
132
+ < button id ="upstream-clear-button "> clear</ button >
133
+ </ div >
134
+ < pre class ="log-stream " id ="upstream-log-stream "> Waiting for upstream logs...</ pre >
135
+ </ div >
72
136
</ div >
73
137
< script >
74
- const logStream = document . getElementById ( 'log-stream' ) ;
75
- const filterInput = document . getElementById ( 'filter-input' ) ;
76
- var logData = "" ;
77
- let regexFilter = null ;
78
-
79
- function setupEventSource ( ) {
80
- if ( typeof ( EventSource ) !== "undefined" ) {
81
- const eventSource = new EventSource ( "/logs/streamSSE" ) ;
82
-
83
- eventSource . onmessage = function ( event ) {
84
- logData += event . data ;
85
- render ( )
86
- } ;
138
+ class LogStream {
139
+ constructor ( streamElement , filterInput , clearButton , endpoint ) {
140
+ this . streamElement = streamElement ;
141
+ this . filterInput = filterInput ;
142
+ this . clearButton = clearButton ;
143
+ this . endpoint = endpoint ;
144
+ this . logData = "" ;
145
+ this . regexFilter = null ;
146
+ this . eventSource = null ;
87
147
88
- eventSource . onerror = function ( err ) {
89
- logData = "EventSource failed: " + err . message ;
90
- } ;
91
- } else {
92
- logData = "SSE Not supported by this browser."
148
+ this . initialize ( ) ;
93
149
}
94
- }
95
150
96
- // poor-ai's react ¯\_(ツ)_/¯
97
- function render ( ) {
98
- if ( regexFilter ) {
99
- const lines = logData . split ( '\n' ) ;
100
- const filteredLines = lines . filter ( line => {
101
- return regexFilter === null || regexFilter . test ( line ) ;
151
+ initialize ( ) {
152
+ this . filterInput . addEventListener ( 'input' , ( ) => this . updateFilter ( ) ) ;
153
+ this . clearButton . addEventListener ( 'click' , ( ) => {
154
+ this . filterInput . value = "" ;
155
+ this . regexFilter = null ;
156
+ this . render ( ) ;
102
157
} ) ;
158
+ this . setupEventSource ( ) ;
159
+ }
103
160
104
- if ( filteredLines . length > 0 ) {
105
- logStream . textContent = filteredLines . join ( '\n' ) + '\n' ;
106
- } else {
107
- logStream . textContent = "" ;
161
+ setupEventSource ( ) {
162
+ if ( typeof ( EventSource ) === "undefined" ) {
163
+ this . logData = "SSE Not supported by this browser." ;
164
+ this . render ( ) ;
165
+ return ;
108
166
}
109
- } else {
110
- logStream . textContent = logData ;
167
+
168
+ const connect = ( ) => {
169
+ this . eventSource = new EventSource ( this . endpoint ) ;
170
+
171
+ this . eventSource . onmessage = ( event ) => {
172
+ this . logData += event . data ;
173
+ this . render ( ) ;
174
+ } ;
175
+
176
+ this . eventSource . onerror = ( err ) => {
177
+ // Close the current connection
178
+ this . eventSource . close ( ) ;
179
+
180
+ this . logData += "\nConnection lost. Retrying in 5 seconds...\n" ;
181
+ this . render ( ) ;
182
+
183
+ // Attempt to reconnect after 5 seconds
184
+ setTimeout ( ( ) => {
185
+ this . logData += "Attempting to reconnect...\n" ;
186
+ this . render ( ) ;
187
+ connect ( ) ;
188
+ } , 5000 ) ;
189
+ } ;
190
+ } ;
191
+
192
+ // Initial connection
193
+ connect ( ) ;
111
194
}
112
195
113
- logStream . scrollTop = logStream . scrollHeight ;
114
- }
196
+ render ( ) {
197
+ let content = this . logData ;
198
+
199
+ if ( this . regexFilter ) {
200
+ const lines = content . split ( '\n' ) ;
201
+ const filteredLines = lines . filter ( line => this . regexFilter . test ( line ) ) ;
202
+ content = filteredLines . length > 0 ? filteredLines . join ( '\n' ) + '\n' : "" ;
203
+ }
204
+
205
+ this . streamElement . textContent = content ;
206
+ this . streamElement . scrollTop = this . streamElement . scrollHeight ;
207
+ }
208
+
209
+ updateFilter ( ) {
210
+ const pattern = this . filterInput . value . trim ( ) ;
211
+ this . filterInput . classList . remove ( 'regex-error' ) ;
212
+
213
+ if ( ! pattern ) {
214
+ this . regexFilter = null ;
215
+ this . render ( ) ;
216
+ return ;
217
+ }
115
218
116
- function updateFilter ( ) {
117
- const pattern = filterInput . value . trim ( ) ;
118
- filterInput . classList . remove ( 'regex-error' ) ;
119
- if ( pattern ) {
120
219
try {
121
- regexFilter = new RegExp ( pattern ) ;
220
+ this . regexFilter = new RegExp ( pattern ) ;
122
221
} catch ( e ) {
123
222
console . error ( "Invalid regex pattern:" , e ) ;
124
- regexFilter = null ;
125
- filterInput . classList . add ( 'regex-error' ) ;
126
- return
223
+ this . regexFilter = null ;
224
+ this . filterInput . classList . add ( 'regex-error' ) ;
225
+ return ;
127
226
}
128
- } else {
129
- regexFilter = null ;
130
- }
131
227
132
- render ( ) ;
228
+ this . render ( ) ;
229
+ }
133
230
}
134
231
135
- filterInput . addEventListener ( 'input' , updateFilter ) ;
136
- document . getElementById ( 'clear-button' ) . addEventListener ( 'click' , ( ) => {
137
- filterInput . value = "" ;
138
- regexFilter = null ;
139
- render ( ) ;
232
+ // Initialize both log streams
233
+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
234
+ new LogStream (
235
+ document . getElementById ( 'proxy-log-stream' ) ,
236
+ document . getElementById ( 'proxy-filter-input' ) ,
237
+ document . getElementById ( 'proxy-clear-button' ) ,
238
+ "/logs/streamSSE/proxy"
239
+ ) ;
240
+
241
+ new LogStream (
242
+ document . getElementById ( 'upstream-log-stream' ) ,
243
+ document . getElementById ( 'upstream-filter-input' ) ,
244
+ document . getElementById ( 'upstream-clear-button' ) ,
245
+ "/logs/streamSSE/upstream"
246
+ ) ;
247
+
248
+ // Initialize clickable headers
249
+ document . querySelectorAll ( 'h2' ) . forEach ( header => {
250
+ header . addEventListener ( 'click' , ( ) => {
251
+ const column = header . closest ( '.log-column' ) ;
252
+ column . classList . toggle ( 'minimized' ) ;
253
+ } ) ;
254
+ } ) ;
140
255
} ) ;
141
- setupEventSource ( ) ;
142
- updateFilter ( ) ;
143
256
</ script >
144
257
</ body >
145
258
</ html >
0 commit comments