orez 0.0.47 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/admin/http-proxy.d.ts.map +1 -1
  2. package/dist/admin/http-proxy.js.map +1 -1
  3. package/dist/admin/log-store.d.ts.map +1 -1
  4. package/dist/admin/log-store.js.map +1 -1
  5. package/dist/admin/server.d.ts +2 -2
  6. package/dist/admin/server.d.ts.map +1 -1
  7. package/dist/admin/server.js.map +1 -1
  8. package/dist/admin/ui.d.ts.map +1 -1
  9. package/dist/admin/ui.js +2 -2
  10. package/dist/admin/ui.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +6 -112
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +0 -5
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +0 -5
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +0 -9
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +91 -249
  21. package/dist/index.js.map +1 -1
  22. package/dist/log.d.ts +0 -9
  23. package/dist/log.d.ts.map +1 -1
  24. package/dist/log.js +1 -24
  25. package/dist/log.js.map +1 -1
  26. package/dist/mutex.d.ts.map +1 -1
  27. package/dist/mutex.js +2 -13
  28. package/dist/mutex.js.map +1 -1
  29. package/dist/pg-proxy.d.ts +2 -3
  30. package/dist/pg-proxy.d.ts.map +1 -1
  31. package/dist/pg-proxy.js +167 -377
  32. package/dist/pg-proxy.js.map +1 -1
  33. package/dist/pglite-manager.d.ts +0 -1
  34. package/dist/pglite-manager.d.ts.map +1 -1
  35. package/dist/pglite-manager.js +1 -1
  36. package/dist/pglite-manager.js.map +1 -1
  37. package/dist/replication/change-tracker.d.ts +0 -6
  38. package/dist/replication/change-tracker.d.ts.map +1 -1
  39. package/dist/replication/change-tracker.js +0 -74
  40. package/dist/replication/change-tracker.js.map +1 -1
  41. package/dist/replication/handler.d.ts.map +1 -1
  42. package/dist/replication/handler.js +5 -47
  43. package/dist/replication/handler.js.map +1 -1
  44. package/dist/vite-plugin.d.ts +0 -3
  45. package/dist/vite-plugin.d.ts.map +1 -1
  46. package/dist/vite-plugin.js +0 -24
  47. package/dist/vite-plugin.js.map +1 -1
  48. package/package.json +5 -4
  49. package/src/admin/http-proxy.ts +5 -1
  50. package/src/admin/log-store.ts +4 -1
  51. package/src/admin/server.ts +7 -3
  52. package/src/admin/ui.ts +682 -680
  53. package/src/cli.ts +6 -111
  54. package/src/config.ts +0 -10
  55. package/src/index.ts +92 -262
  56. package/src/integration/integration.test.ts +264 -133
  57. package/src/log.ts +1 -25
  58. package/src/mutex.ts +2 -12
  59. package/src/pg-proxy.ts +187 -449
  60. package/src/pglite-manager.ts +1 -1
  61. package/src/replication/change-tracker.ts +0 -92
  62. package/src/replication/handler.ts +4 -50
  63. package/src/shim/hooks.mjs +34 -1
  64. package/src/vite-plugin.ts +0 -28
  65. package/src/wasm-sqlite.test.ts +1 -2
package/src/admin/ui.ts CHANGED
@@ -1,682 +1,684 @@
1
1
  export function getAdminHtml(): string {
2
- return '<!DOCTYPE html>\n' +
3
- '<html lang="en">\n' +
4
- '<head>\n' +
5
- '<meta charset="utf-8">\n' +
6
- '<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
7
- '<title>orez admin</title>\n' +
8
- '<style>\n' +
9
- ':root {\n' +
10
- ' --bg: #0d1117;\n' +
11
- ' --surface: #161b22;\n' +
12
- ' --border: #30363d;\n' +
13
- ' --text: #e6edf3;\n' +
14
- ' --text-dim: #8b949e;\n' +
15
- ' --accent: #58a6ff;\n' +
16
- ' --green: #3fb950;\n' +
17
- ' --yellow: #d29922;\n' +
18
- ' --red: #f85149;\n' +
19
- ' --purple: #bc8cff;\n' +
20
- '}\n' +
21
- '* { margin: 0; padding: 0; box-sizing: border-box; }\n' +
22
- 'body {\n' +
23
- ' font-family: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", monospace;\n' +
24
- ' background: var(--bg);\n' +
25
- ' color: var(--text);\n' +
26
- ' height: 100vh;\n' +
27
- ' display: flex;\n' +
28
- ' flex-direction: column;\n' +
29
- ' overflow: hidden;\n' +
30
- '}\n' +
31
- '.header {\n' +
32
- ' display: flex;\n' +
33
- ' align-items: center;\n' +
34
- ' padding: 12px 16px;\n' +
35
- ' background: var(--surface);\n' +
36
- ' border-bottom: 1px solid var(--border);\n' +
37
- ' gap: 12px;\n' +
38
- ' flex-shrink: 0;\n' +
39
- '}\n' +
40
- '.header .logo {\n' +
41
- ' font-size: 15px;\n' +
42
- ' font-weight: 700;\n' +
43
- ' color: var(--accent);\n' +
44
- ' letter-spacing: -0.5px;\n' +
45
- '}\n' +
46
- '.badge {\n' +
47
- ' display: inline-flex;\n' +
48
- ' align-items: center;\n' +
49
- ' padding: 2px 8px;\n' +
50
- ' border-radius: 12px;\n' +
51
- ' font-size: 11px;\n' +
52
- ' border: 1px solid var(--border);\n' +
53
- ' color: var(--text-dim);\n' +
54
- ' gap: 4px;\n' +
55
- '}\n' +
56
- '.badge .dot {\n' +
57
- ' width: 6px;\n' +
58
- ' height: 6px;\n' +
59
- ' border-radius: 50%;\n' +
60
- ' background: var(--green);\n' +
61
- '}\n' +
62
- '.spacer { flex: 1; }\n' +
63
- '.tabs {\n' +
64
- ' display: flex;\n' +
65
- ' padding: 0 16px;\n' +
66
- ' background: var(--surface);\n' +
67
- ' border-bottom: 1px solid var(--border);\n' +
68
- ' gap: 2px;\n' +
69
- ' flex-shrink: 0;\n' +
70
- '}\n' +
71
- '.tab {\n' +
72
- ' padding: 8px 14px;\n' +
73
- ' font-size: 12px;\n' +
74
- ' color: var(--text-dim);\n' +
75
- ' cursor: pointer;\n' +
76
- ' border-bottom: 2px solid transparent;\n' +
77
- ' transition: all 0.15s;\n' +
78
- ' background: none;\n' +
79
- ' border-top: none;\n' +
80
- ' border-left: none;\n' +
81
- ' border-right: none;\n' +
82
- ' font-family: inherit;\n' +
83
- '}\n' +
84
- '.tab:hover { color: var(--text); }\n' +
85
- '.tab.active {\n' +
86
- ' color: var(--accent);\n' +
87
- ' border-bottom-color: var(--accent);\n' +
88
- '}\n' +
89
- '.toolbar {\n' +
90
- ' display: flex;\n' +
91
- ' align-items: center;\n' +
92
- ' padding: 8px 16px;\n' +
93
- ' gap: 10px;\n' +
94
- ' border-bottom: 1px solid var(--border);\n' +
95
- ' flex-shrink: 0;\n' +
96
- '}\n' +
97
- '.toolbar label {\n' +
98
- ' font-size: 11px;\n' +
99
- ' color: var(--text-dim);\n' +
100
- ' text-transform: uppercase;\n' +
101
- ' letter-spacing: 0.5px;\n' +
102
- '}\n' +
103
- '.toolbar select {\n' +
104
- ' background: var(--surface);\n' +
105
- ' color: var(--text);\n' +
106
- ' border: 1px solid var(--border);\n' +
107
- ' border-radius: 6px;\n' +
108
- ' padding: 4px 8px;\n' +
109
- ' font-size: 12px;\n' +
110
- ' font-family: inherit;\n' +
111
- ' cursor: pointer;\n' +
112
- '}\n' +
113
- '.toolbar select:focus { outline: none; border-color: var(--accent); }\n' +
114
- '.toolbar input[type="text"] {\n' +
115
- ' background: var(--surface);\n' +
116
- ' color: var(--text);\n' +
117
- ' border: 1px solid var(--border);\n' +
118
- ' border-radius: 6px;\n' +
119
- ' padding: 4px 8px;\n' +
120
- ' font-size: 12px;\n' +
121
- ' font-family: inherit;\n' +
122
- ' width: 200px;\n' +
123
- '}\n' +
124
- '.toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }\n' +
125
- '.toolbar input[type="text"]::placeholder { color: var(--text-dim); }\n' +
126
- '.sep { width: 1px; height: 20px; background: var(--border); }\n' +
127
- '.action-btn {\n' +
128
- ' padding: 5px 12px;\n' +
129
- ' border-radius: 6px;\n' +
130
- ' border: 1px solid;\n' +
131
- ' background: transparent;\n' +
132
- ' cursor: pointer;\n' +
133
- ' font-family: inherit;\n' +
134
- ' font-size: 11px;\n' +
135
- ' transition: all 0.15s ease;\n' +
136
- ' white-space: nowrap;\n' +
137
- '}\n' +
138
- '.action-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n' +
139
- '.action-btn.blue { color: var(--accent); border-color: #1f6feb44; }\n' +
140
- '.action-btn.blue:hover:not(:disabled) { background: #1f6feb22; border-color: var(--accent); }\n' +
141
- '.action-btn.orange { color: var(--yellow); border-color: #d2992244; }\n' +
142
- '.action-btn.orange:hover:not(:disabled) { background: #d2992222; border-color: var(--yellow); }\n' +
143
- '.action-btn.red { color: var(--red); border-color: #f8514944; }\n' +
144
- '.action-btn.red:hover:not(:disabled) { background: #f8514922; border-color: var(--red); }\n' +
145
- '.action-btn.gray { color: var(--text-dim); border-color: #8b949e44; }\n' +
146
- '.action-btn.gray:hover:not(:disabled) { background: #8b949e22; border-color: var(--text-dim); }\n' +
147
- '.content-area {\n' +
148
- ' flex: 1;\n' +
149
- ' overflow: hidden;\n' +
150
- ' position: relative;\n' +
151
- ' display: flex;\n' +
152
- ' flex-direction: column;\n' +
153
- '}\n' +
154
- '.log-wrap {\n' +
155
- ' flex: 1;\n' +
156
- ' overflow: hidden;\n' +
157
- ' position: relative;\n' +
158
- '}\n' +
159
- '.log-view {\n' +
160
- ' height: 100%;\n' +
161
- ' overflow-y: auto;\n' +
162
- ' padding: 8px 16px;\n' +
163
- ' font-size: 12px;\n' +
164
- ' line-height: 1.5;\n' +
165
- '}\n' +
166
- '.log-line { white-space: pre-wrap; word-break: break-all; }\n' +
167
- '.log-line .ts { color: var(--text-dim); }\n' +
168
- '.log-line .src { display: inline-block; width: 7ch; }\n' +
169
- '.log-line .src.zero { color: var(--purple); }\n' +
170
- '.log-line .src.pglite { color: var(--green); }\n' +
171
- '.log-line .src.proxy { color: var(--yellow); }\n' +
172
- '.log-line .src.orez { color: var(--accent); }\n' +
173
- '.log-line .src.s3 { color: #79c0ff; }\n' +
174
- '.log-line.level-error .msg { color: var(--red); }\n' +
175
- '.log-line.level-warn .msg { color: var(--yellow); }\n' +
176
- '.log-line.level-info .msg { color: var(--text); }\n' +
177
- '.log-line.level-debug .msg { color: var(--text-dim); }\n' +
178
- '.jump-btn {\n' +
179
- ' position: absolute;\n' +
180
- ' bottom: 16px;\n' +
181
- ' left: 50%;\n' +
182
- ' transform: translateX(-50%);\n' +
183
- ' padding: 6px 16px;\n' +
184
- ' border-radius: 20px;\n' +
185
- ' background: var(--accent);\n' +
186
- ' color: #fff;\n' +
187
- ' border: none;\n' +
188
- ' font-size: 12px;\n' +
189
- ' font-family: inherit;\n' +
190
- ' cursor: pointer;\n' +
191
- ' opacity: 0;\n' +
192
- ' transition: opacity 0.2s;\n' +
193
- ' pointer-events: none;\n' +
194
- ' z-index: 10;\n' +
195
- '}\n' +
196
- '.jump-btn.visible { opacity: 1; pointer-events: auto; }\n' +
197
- '.env-view {\n' +
198
- ' height: 100%;\n' +
199
- ' overflow-y: auto;\n' +
200
- ' padding: 16px;\n' +
201
- ' display: none;\n' +
202
- '}\n' +
203
- '.env-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
204
- '.env-table th {\n' +
205
- ' text-align: left;\n' +
206
- ' padding: 6px 12px;\n' +
207
- ' color: var(--text-dim);\n' +
208
- ' border-bottom: 1px solid var(--border);\n' +
209
- ' font-weight: 500;\n' +
210
- ' text-transform: uppercase;\n' +
211
- ' font-size: 10px;\n' +
212
- ' letter-spacing: 0.5px;\n' +
213
- '}\n' +
214
- '.env-table td {\n' +
215
- ' padding: 6px 12px;\n' +
216
- ' border-bottom: 1px solid #21262d;\n' +
217
- '}\n' +
218
- '.env-table td:first-child { color: var(--accent); white-space: nowrap; }\n' +
219
- '.env-table td:last-child { color: var(--text); word-break: break-all; }\n' +
220
- '.env-table tr:hover td { background: #161b22; }\n' +
221
- // http view
222
- '.http-view {\n' +
223
- ' height: 100%;\n' +
224
- ' overflow-y: auto;\n' +
225
- ' padding: 0;\n' +
226
- ' display: none;\n' +
227
- '}\n' +
228
- '.http-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
229
- '.http-table th {\n' +
230
- ' text-align: left;\n' +
231
- ' padding: 6px 12px;\n' +
232
- ' color: var(--text-dim);\n' +
233
- ' border-bottom: 1px solid var(--border);\n' +
234
- ' font-weight: 500;\n' +
235
- ' text-transform: uppercase;\n' +
236
- ' font-size: 10px;\n' +
237
- ' letter-spacing: 0.5px;\n' +
238
- ' position: sticky;\n' +
239
- ' top: 0;\n' +
240
- ' background: var(--bg);\n' +
241
- ' z-index: 1;\n' +
242
- '}\n' +
243
- '.http-table td {\n' +
244
- ' padding: 5px 12px;\n' +
245
- ' border-bottom: 1px solid #21262d;\n' +
246
- ' white-space: nowrap;\n' +
247
- '}\n' +
248
- '.http-table tr.http-row { cursor: pointer; }\n' +
249
- '.http-table tr.http-row:hover td { background: #161b22; }\n' +
250
- '.http-table .method { font-weight: 600; }\n' +
251
- '.http-table .method.get { color: var(--green); }\n' +
252
- '.http-table .method.post { color: var(--yellow); }\n' +
253
- '.http-table .method.put { color: var(--accent); }\n' +
254
- '.http-table .method.delete { color: var(--red); }\n' +
255
- '.http-table .method.patch { color: #79c0ff; }\n' +
256
- '.http-table .method.ws { color: var(--purple); }\n' +
257
- '.http-table .status.s2 { color: var(--green); }\n' +
258
- '.http-table .status.s3 { color: var(--yellow); }\n' +
259
- '.http-table .status.s4 { color: var(--red); }\n' +
260
- '.http-table .status.s5 { color: var(--red); font-weight: 600; }\n' +
261
- '.http-table .path { color: var(--text); max-width: 500px; overflow: hidden; text-overflow: ellipsis; }\n' +
262
- '.http-table .dur { color: var(--text-dim); }\n' +
263
- '.http-table .sz { color: var(--text-dim); }\n' +
264
- '.http-detail {\n' +
265
- ' display: none;\n' +
266
- '}\n' +
267
- '.http-detail.open { display: table-row; }\n' +
268
- '.http-detail td {\n' +
269
- ' padding: 8px 12px 12px 24px;\n' +
270
- ' background: #0c0e14;\n' +
271
- ' border-bottom: 1px solid var(--border);\n' +
272
- '}\n' +
273
- '.http-detail .hdr-section { margin-bottom: 8px; }\n' +
274
- '.http-detail .hdr-title {\n' +
275
- ' font-size: 10px;\n' +
276
- ' text-transform: uppercase;\n' +
277
- ' color: var(--text-dim);\n' +
278
- ' letter-spacing: 0.5px;\n' +
279
- ' margin-bottom: 4px;\n' +
280
- '}\n' +
281
- '.http-detail .hdr-line {\n' +
282
- ' font-size: 11px;\n' +
283
- ' line-height: 1.6;\n' +
284
- ' white-space: pre-wrap;\n' +
285
- ' word-break: break-all;\n' +
286
- '}\n' +
287
- '.http-detail .hdr-key { color: var(--accent); }\n' +
288
- '.http-detail .hdr-val { color: var(--text-dim); }\n' +
289
- // actions panel
290
- '.actions-panel {\n' +
291
- ' flex-shrink: 0;\n' +
292
- ' border-top: 1px solid var(--border);\n' +
293
- ' background: var(--surface);\n' +
294
- ' padding: 8px 16px;\n' +
295
- '}\n' +
296
- '.action-row {\n' +
297
- ' display: flex;\n' +
298
- ' align-items: center;\n' +
299
- ' gap: 8px;\n' +
300
- ' padding: 4px 0;\n' +
301
- '}\n' +
302
- '.action-label {\n' +
303
- ' font-size: 11px;\n' +
304
- ' font-weight: 600;\n' +
305
- ' width: 7ch;\n' +
306
- ' flex-shrink: 0;\n' +
307
- '}\n' +
308
- '.action-label.zero { color: var(--purple); }\n' +
309
- '.action-label.logs { color: var(--text-dim); }\n' +
310
- // toast
311
- '.toast {\n' +
312
- ' position: fixed;\n' +
313
- ' bottom: 20px;\n' +
314
- ' right: 20px;\n' +
315
- ' padding: 10px 16px;\n' +
316
- ' border-radius: 8px;\n' +
317
- ' background: var(--surface);\n' +
318
- ' border: 1px solid var(--border);\n' +
319
- ' color: var(--text);\n' +
320
- ' font-size: 12px;\n' +
321
- ' font-family: inherit;\n' +
322
- ' opacity: 0;\n' +
323
- ' transform: translateY(10px);\n' +
324
- ' transition: all 0.3s ease;\n' +
325
- ' pointer-events: none;\n' +
326
- ' z-index: 100;\n' +
327
- '}\n' +
328
- '.toast.show { opacity: 1; transform: translateY(0); }\n' +
329
- '.toast.error { border-color: var(--red); color: var(--red); }\n' +
330
- '.toast.success { border-color: var(--green); color: var(--green); }\n' +
331
- '</style>\n' +
332
- '</head>\n' +
333
- '<body>\n' +
334
- ' <div class="header">\n' +
335
- ' <span class="logo">&#9670; orez admin</span>\n' +
336
- ' <div class="spacer"></div>\n' +
337
- ' <span class="badge"><span class="dot"></span> pg <span id="pg-port">-</span></span>\n' +
338
- ' <span class="badge"><span class="dot"></span> zero <span id="zero-port">-</span></span>\n' +
339
- ' <span class="badge" id="uptime-badge">&#9201; --</span>\n' +
340
- ' </div>\n' +
341
- '\n' +
342
- ' <div class="tabs" id="tab-bar">\n' +
343
- ' <button class="tab active" data-source="">All</button>\n' +
344
- ' <button class="tab" data-source="zero">Zero</button>\n' +
345
- ' <button class="tab" data-source="pglite">PGlite</button>\n' +
346
- ' <button class="tab" data-source="proxy">Proxy</button>\n' +
347
- ' <button class="tab" data-source="orez">Orez</button>\n' +
348
- ' <button class="tab" data-source="s3">S3</button>\n' +
349
- ' <button class="tab" data-source="http">HTTP</button>\n' +
350
- ' <button class="tab" data-source="env">Env</button>\n' +
351
- ' </div>\n' +
352
- '\n' +
353
- ' <div class="toolbar" id="toolbar">\n' +
354
- ' <label>Level</label>\n' +
355
- ' <select id="level-filter">\n' +
356
- ' <option value="" selected>all levels</option>\n' +
357
- ' <option value="error">error only</option>\n' +
358
- ' <option value="warn">warn+</option>\n' +
359
- ' <option value="info">info+</option>\n' +
360
- ' </select>\n' +
361
- ' </div>\n' +
362
- '\n' +
363
- ' <div class="toolbar" id="http-toolbar" style="display:none">\n' +
364
- ' <label>Filter</label>\n' +
365
- ' <input type="text" id="http-path-filter" placeholder="filter by path...">\n' +
366
- ' </div>\n' +
367
- '\n' +
368
- ' <div class="content-area">\n' +
369
- ' <div class="log-wrap">\n' +
370
- ' <div class="log-view" id="log-view"></div>\n' +
371
- ' <div class="env-view" id="env-view">\n' +
372
- ' <table class="env-table">\n' +
373
- ' <thead><tr><th>Variable</th><th>Value</th></tr></thead>\n' +
374
- ' <tbody id="env-body"></tbody>\n' +
375
- ' </table>\n' +
376
- ' </div>\n' +
377
- ' <div class="http-view" id="http-view">\n' +
378
- ' <table class="http-table">\n' +
379
- ' <thead><tr>\n' +
380
- ' <th>Time</th>\n' +
381
- ' <th>Method</th>\n' +
382
- ' <th>Path</th>\n' +
383
- ' <th>Status</th>\n' +
384
- ' <th>Duration</th>\n' +
385
- ' <th>Size</th>\n' +
386
- ' </tr></thead>\n' +
387
- ' <tbody id="http-body"></tbody>\n' +
388
- ' </table>\n' +
389
- ' </div>\n' +
390
- ' <button class="jump-btn" id="jump-btn" onclick="jumpToBottom()">&#x2193; Jump to bottom</button>\n' +
391
- ' </div>\n' +
392
- '\n' +
393
- ' <div class="actions-panel" id="actions-panel">\n' +
394
- ' <div class="action-row">\n' +
395
- ' <span class="action-label zero">zero</span>\n' +
396
- ' <button class="action-btn blue" data-zero-action onclick="doAction(\'restart-zero\', this)">&#x21bb; Restart</button>\n' +
397
- ' <button class="action-btn orange" data-zero-action onclick="doAction(\'reset-zero\', this)">&#x21ba; Reset</button>\n' +
398
- ' </div>\n' +
399
- ' <div class="action-row">\n' +
400
- ' <span class="action-label logs">logs</span>\n' +
401
- ' <button class="action-btn gray" onclick="doAction(\'clear-logs\', this)">&#x2715; Clear Logs</button>\n' +
402
- ' <button class="action-btn gray" onclick="doAction(\'clear-http\', this)">&#x2715; Clear HTTP</button>\n' +
403
- ' </div>\n' +
404
- ' </div>\n' +
405
- ' </div>\n' +
406
- '\n' +
407
- ' <div class="toast" id="toast"></div>\n' +
408
- '\n' +
409
- '<script>\n' +
410
- 'var activeSource = "";\n' +
411
- 'var activeLevel = "";\n' +
412
- 'var lastCursor = 0;\n' +
413
- 'var autoScroll = true;\n' +
414
- 'var envLoaded = false;\n' +
415
- 'var isEnvTab = false;\n' +
416
- 'var isHttpTab = false;\n' +
417
- 'var httpCursor = 0;\n' +
418
- 'var httpAutoScroll = true;\n' +
419
- '\n' +
420
- 'var logView = document.getElementById("log-view");\n' +
421
- 'var envView = document.getElementById("env-view");\n' +
422
- 'var httpView = document.getElementById("http-view");\n' +
423
- 'var jumpBtn = document.getElementById("jump-btn");\n' +
424
- 'var toastEl = document.getElementById("toast");\n' +
425
- 'var toolbar = document.getElementById("toolbar");\n' +
426
- 'var httpToolbar = document.getElementById("http-toolbar");\n' +
427
- '\n' +
428
- 'document.getElementById("tab-bar").addEventListener("click", function(e) {\n' +
429
- ' var tab = e.target.closest(".tab");\n' +
430
- ' if (!tab) return;\n' +
431
- ' document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });\n' +
432
- ' tab.classList.add("active");\n' +
433
- ' var source = tab.dataset.source;\n' +
434
- ' isEnvTab = source === "env";\n' +
435
- ' isHttpTab = source === "http";\n' +
436
- ' logView.style.display = "none";\n' +
437
- ' envView.style.display = "none";\n' +
438
- ' httpView.style.display = "none";\n' +
439
- ' toolbar.style.display = "none";\n' +
440
- ' httpToolbar.style.display = "none";\n' +
441
- ' if (isEnvTab) {\n' +
442
- ' envView.style.display = "block";\n' +
443
- ' if (!envLoaded) loadEnv();\n' +
444
- ' } else if (isHttpTab) {\n' +
445
- ' httpView.style.display = "block";\n' +
446
- ' httpToolbar.style.display = "flex";\n' +
447
- ' httpCursor = 0;\n' +
448
- ' document.getElementById("http-body").innerHTML = "";\n' +
449
- ' fetchHttp();\n' +
450
- ' } else {\n' +
451
- ' logView.style.display = "block";\n' +
452
- ' toolbar.style.display = "flex";\n' +
453
- ' activeSource = source;\n' +
454
- ' lastCursor = 0;\n' +
455
- ' logView.innerHTML = "";\n' +
456
- ' fetchLogs();\n' +
457
- ' }\n' +
458
- '});\n' +
459
- '\n' +
460
- 'document.getElementById("level-filter").addEventListener("change", function(e) {\n' +
461
- ' activeLevel = e.target.value;\n' +
462
- ' lastCursor = 0;\n' +
463
- ' logView.innerHTML = "";\n' +
464
- ' fetchLogs();\n' +
465
- '});\n' +
466
- '\n' +
467
- 'var httpFilterTimeout = null;\n' +
468
- 'document.getElementById("http-path-filter").addEventListener("input", function() {\n' +
469
- ' clearTimeout(httpFilterTimeout);\n' +
470
- ' httpFilterTimeout = setTimeout(function() {\n' +
471
- ' httpCursor = 0;\n' +
472
- ' document.getElementById("http-body").innerHTML = "";\n' +
473
- ' fetchHttp();\n' +
474
- ' }, 300);\n' +
475
- '});\n' +
476
- '\n' +
477
- 'logView.addEventListener("scroll", function() {\n' +
478
- ' var atBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 40;\n' +
479
- ' autoScroll = atBottom;\n' +
480
- ' jumpBtn.classList.toggle("visible", !atBottom);\n' +
481
- '});\n' +
482
- '\n' +
483
- 'httpView.addEventListener("scroll", function() {\n' +
484
- ' var atBottom = httpView.scrollHeight - httpView.scrollTop - httpView.clientHeight < 40;\n' +
485
- ' httpAutoScroll = atBottom;\n' +
486
- '});\n' +
487
- '\n' +
488
- 'function jumpToBottom() {\n' +
489
- ' var el = isHttpTab ? httpView : logView;\n' +
490
- ' el.scrollTop = el.scrollHeight;\n' +
491
- ' autoScroll = true;\n' +
492
- ' httpAutoScroll = true;\n' +
493
- ' jumpBtn.classList.remove("visible");\n' +
494
- '}\n' +
495
- '\n' +
496
- 'function fmtTime(ts) {\n' +
497
- ' var d = new Date(ts);\n' +
498
- ' return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })\n' +
499
- ' + "." + String(d.getMilliseconds()).padStart(3, "0");\n' +
500
- '}\n' +
501
- '\n' +
502
- 'function escHtml(s) {\n' +
503
- ' return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");\n' +
504
- '}\n' +
505
- '\n' +
506
- 'function fmtSize(bytes) {\n' +
507
- ' if (bytes === 0) return "-";\n' +
508
- ' if (bytes < 1024) return bytes + "B";\n' +
509
- ' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "kb";\n' +
510
- ' return (bytes / (1024 * 1024)).toFixed(1) + "MB";\n' +
511
- '}\n' +
512
- '\n' +
513
- 'function renderEntries(entries) {\n' +
514
- ' var frag = document.createDocumentFragment();\n' +
515
- ' for (var i = 0; i < entries.length; i++) {\n' +
516
- ' var e = entries[i];\n' +
517
- ' var div = document.createElement("div");\n' +
518
- ' div.className = "log-line level-" + e.level;\n' +
519
- ' div.innerHTML = \'<span class="ts">\' + fmtTime(e.ts) + "</span> "\n' +
520
- ' + \'<span class="src \' + e.source + \'">\' + e.source.padEnd(6) + "</span> "\n' +
521
- ' + \'<span class="msg">\' + escHtml(e.msg) + "</span>";\n' +
522
- ' frag.appendChild(div);\n' +
523
- ' }\n' +
524
- ' logView.appendChild(frag);\n' +
525
- ' if (autoScroll) logView.scrollTop = logView.scrollHeight;\n' +
526
- '}\n' +
527
- '\n' +
528
- 'function renderHttpEntries(entries) {\n' +
529
- ' var tbody = document.getElementById("http-body");\n' +
530
- ' var frag = document.createDocumentFragment();\n' +
531
- ' for (var i = 0; i < entries.length; i++) {\n' +
532
- ' var e = entries[i];\n' +
533
- ' var tr = document.createElement("tr");\n' +
534
- ' tr.className = "http-row";\n' +
535
- ' tr.dataset.id = e.id;\n' +
536
- ' var mc = e.method.toLowerCase();\n' +
537
- ' var sc = "s" + String(e.status).charAt(0);\n' +
538
- ' tr.innerHTML = "<td>" + fmtTime(e.ts) + "</td>"\n' +
539
- ' + \'<td><span class="method \' + mc + \'">\' + e.method + "</span></td>"\n' +
540
- ' + \'<td class="path">\' + escHtml(e.path) + "</td>"\n' +
541
- ' + \'<td><span class="status \' + sc + \'">\' + e.status + "</span></td>"\n' +
542
- ' + \'<td class="dur">\' + e.duration + "ms</td>"\n' +
543
- ' + \'<td class="sz">\' + fmtSize(e.resSize) + "</td>";\n' +
544
- ' tr.addEventListener("click", (function(entry) {\n' +
545
- ' return function() { toggleHttpDetail(this, entry); };\n' +
546
- ' })(e));\n' +
547
- ' frag.appendChild(tr);\n' +
548
- ' }\n' +
549
- ' tbody.appendChild(frag);\n' +
550
- ' if (httpAutoScroll) httpView.scrollTop = httpView.scrollHeight;\n' +
551
- '}\n' +
552
- '\n' +
553
- 'function toggleHttpDetail(row, entry) {\n' +
554
- ' var next = row.nextElementSibling;\n' +
555
- ' if (next && next.classList.contains("http-detail")) {\n' +
556
- ' next.classList.toggle("open");\n' +
557
- ' return;\n' +
558
- ' }\n' +
559
- ' var detail = document.createElement("tr");\n' +
560
- ' detail.className = "http-detail open";\n' +
561
- ' var html = \'<td colspan="6">\';\n' +
562
- ' html += \'<div class="hdr-section"><div class="hdr-title">request headers</div>\';\n' +
563
- ' var rk = Object.keys(entry.reqHeaders || {}).sort();\n' +
564
- ' for (var i = 0; i < rk.length; i++) {\n' +
565
- ' html += \'<div class="hdr-line"><span class="hdr-key">\' + escHtml(rk[i]) + \'</span>: <span class="hdr-val">\' + escHtml(entry.reqHeaders[rk[i]]) + "</span></div>";\n' +
566
- ' }\n' +
567
- ' html += "</div>";\n' +
568
- ' html += \'<div class="hdr-section"><div class="hdr-title">response headers</div>\';\n' +
569
- ' var sk = Object.keys(entry.resHeaders || {}).sort();\n' +
570
- ' for (var j = 0; j < sk.length; j++) {\n' +
571
- ' html += \'<div class="hdr-line"><span class="hdr-key">\' + escHtml(sk[j]) + \'</span>: <span class="hdr-val">\' + escHtml(entry.resHeaders[sk[j]]) + "</span></div>";\n' +
572
- ' }\n' +
573
- ' html += "</div>";\n' +
574
- ' if (entry.reqSize > 0) html += \'<div class="hdr-line"><span class="hdr-key">request body size</span>: <span class="hdr-val">\' + fmtSize(entry.reqSize) + "</span></div>";\n' +
575
- ' html += "</td>";\n' +
576
- ' detail.innerHTML = html;\n' +
577
- ' row.parentNode.insertBefore(detail, row.nextSibling);\n' +
578
- '}\n' +
579
- '\n' +
580
- 'function fetchLogs() {\n' +
581
- ' var params = new URLSearchParams();\n' +
582
- ' if (activeSource) params.set("source", activeSource);\n' +
583
- ' if (activeLevel) params.set("level", activeLevel);\n' +
584
- ' if (lastCursor) params.set("since", String(lastCursor));\n' +
585
- ' fetch("/api/logs?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
586
- ' if (data.entries && data.entries.length > 0) renderEntries(data.entries);\n' +
587
- ' if (data.cursor) lastCursor = data.cursor;\n' +
588
- ' }).catch(function() {});\n' +
589
- '}\n' +
590
- '\n' +
591
- 'function fetchHttp() {\n' +
592
- ' var params = new URLSearchParams();\n' +
593
- ' if (httpCursor) params.set("since", String(httpCursor));\n' +
594
- ' var pathFilter = document.getElementById("http-path-filter").value;\n' +
595
- ' if (pathFilter) params.set("path", pathFilter);\n' +
596
- ' fetch("/api/http-log?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
597
- ' if (data.entries && data.entries.length > 0) renderHttpEntries(data.entries);\n' +
598
- ' if (data.cursor) httpCursor = data.cursor;\n' +
599
- ' }).catch(function() {});\n' +
600
- '}\n' +
601
- '\n' +
602
- 'function loadEnv() {\n' +
603
- ' fetch("/api/env").then(function(res) { return res.json(); }).then(function(data) {\n' +
604
- ' var tbody = document.getElementById("env-body");\n' +
605
- ' tbody.innerHTML = "";\n' +
606
- ' var keys = Object.keys(data.env).sort();\n' +
607
- ' for (var i = 0; i < keys.length; i++) {\n' +
608
- ' var tr = document.createElement("tr");\n' +
609
- ' tr.innerHTML = "<td>" + escHtml(keys[i]) + "</td><td>" + escHtml(String(data.env[keys[i]])) + "</td>";\n' +
610
- ' tbody.appendChild(tr);\n' +
611
- ' }\n' +
612
- ' envLoaded = true;\n' +
613
- ' }).catch(function() {});\n' +
614
- '}\n' +
615
- '\n' +
616
- 'function fetchStatus() {\n' +
617
- ' fetch("/api/status").then(function(res) { return res.json(); }).then(function(data) {\n' +
618
- ' document.getElementById("pg-port").textContent = ":" + data.pgPort;\n' +
619
- ' document.getElementById("zero-port").textContent = ":" + data.zeroPort;\n' +
620
- ' var m = Math.floor(data.uptime / 60);\n' +
621
- ' var s = data.uptime % 60;\n' +
622
- ' document.getElementById("uptime-badge").textContent = "\\u23F1 " + (m > 0 ? m + "m " : "") + s + "s";\n' +
623
- ' var zeroDisabled = data.skipZeroCache;\n' +
624
- ' document.querySelectorAll("[data-zero-action]").forEach(function(btn) {\n' +
625
- ' btn.disabled = zeroDisabled;\n' +
626
- ' });\n' +
627
- ' }).catch(function() {});\n' +
628
- '}\n' +
629
- '\n' +
630
- 'function doAction(action, btn) {\n' +
631
- ' if (action === "reset-zero") {\n' +
632
- ' if (!confirm("Reset zero-cache? This deletes the replica and resyncs from scratch.")) return;\n' +
633
- ' }\n' +
634
- ' btn.disabled = true;\n' +
635
- ' var origText = btn.textContent;\n' +
636
- ' btn.textContent = "...";\n' +
637
- ' fetch("/api/actions/" + action, { method: "POST" })\n' +
638
- ' .then(function(res) { return res.json(); })\n' +
639
- ' .then(function(data) {\n' +
640
- ' showToast(data.message || "done", data.ok ? "success" : "error");\n' +
641
- ' if (action === "clear-logs") {\n' +
642
- ' logView.innerHTML = "";\n' +
643
- ' lastCursor = 0;\n' +
644
- ' }\n' +
645
- ' if (action === "clear-http") {\n' +
646
- ' document.getElementById("http-body").innerHTML = "";\n' +
647
- ' httpCursor = 0;\n' +
648
- ' }\n' +
649
- ' })\n' +
650
- ' .catch(function(err) {\n' +
651
- ' showToast("failed: " + err.message, "error");\n' +
652
- ' })\n' +
653
- ' .finally(function() {\n' +
654
- ' btn.disabled = false;\n' +
655
- ' btn.textContent = origText;\n' +
656
- ' });\n' +
657
- '}\n' +
658
- '\n' +
659
- 'function showToast(msg, type) {\n' +
660
- ' toastEl.textContent = msg;\n' +
661
- ' toastEl.className = "toast " + type + " show";\n' +
662
- ' setTimeout(function() { toastEl.className = "toast"; }, 2500);\n' +
663
- '}\n' +
664
- '\n' +
665
- 'fetchLogs();\n' +
666
- 'fetchStatus();\n' +
667
- 'setInterval(function() {\n' +
668
- ' if (document.hidden) return;\n' +
669
- ' if (isHttpTab) fetchHttp();\n' +
670
- ' else if (!isEnvTab) fetchLogs();\n' +
671
- '}, 1000);\n' +
672
- 'setInterval(function() { if (!document.hidden) fetchStatus(); }, 5000);\n' +
673
- 'document.addEventListener("visibilitychange", function() {\n' +
674
- ' if (document.hidden) return;\n' +
675
- ' if (isHttpTab) fetchHttp();\n' +
676
- ' else if (!isEnvTab) fetchLogs();\n' +
677
- ' fetchStatus();\n' +
678
- '});\n' +
679
- '</script>\n' +
680
- '</body>\n' +
681
- '</html>'
2
+ return (
3
+ '<!DOCTYPE html>\n' +
4
+ '<html lang="en">\n' +
5
+ '<head>\n' +
6
+ '<meta charset="utf-8">\n' +
7
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
8
+ '<title>orez admin</title>\n' +
9
+ '<style>\n' +
10
+ ':root {\n' +
11
+ ' --bg: #0d1117;\n' +
12
+ ' --surface: #161b22;\n' +
13
+ ' --border: #30363d;\n' +
14
+ ' --text: #e6edf3;\n' +
15
+ ' --text-dim: #8b949e;\n' +
16
+ ' --accent: #58a6ff;\n' +
17
+ ' --green: #3fb950;\n' +
18
+ ' --yellow: #d29922;\n' +
19
+ ' --red: #f85149;\n' +
20
+ ' --purple: #bc8cff;\n' +
21
+ '}\n' +
22
+ '* { margin: 0; padding: 0; box-sizing: border-box; }\n' +
23
+ 'body {\n' +
24
+ ' font-family: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", monospace;\n' +
25
+ ' background: var(--bg);\n' +
26
+ ' color: var(--text);\n' +
27
+ ' height: 100vh;\n' +
28
+ ' display: flex;\n' +
29
+ ' flex-direction: column;\n' +
30
+ ' overflow: hidden;\n' +
31
+ '}\n' +
32
+ '.header {\n' +
33
+ ' display: flex;\n' +
34
+ ' align-items: center;\n' +
35
+ ' padding: 12px 16px;\n' +
36
+ ' background: var(--surface);\n' +
37
+ ' border-bottom: 1px solid var(--border);\n' +
38
+ ' gap: 12px;\n' +
39
+ ' flex-shrink: 0;\n' +
40
+ '}\n' +
41
+ '.header .logo {\n' +
42
+ ' font-size: 15px;\n' +
43
+ ' font-weight: 700;\n' +
44
+ ' color: var(--accent);\n' +
45
+ ' letter-spacing: -0.5px;\n' +
46
+ '}\n' +
47
+ '.badge {\n' +
48
+ ' display: inline-flex;\n' +
49
+ ' align-items: center;\n' +
50
+ ' padding: 2px 8px;\n' +
51
+ ' border-radius: 12px;\n' +
52
+ ' font-size: 11px;\n' +
53
+ ' border: 1px solid var(--border);\n' +
54
+ ' color: var(--text-dim);\n' +
55
+ ' gap: 4px;\n' +
56
+ '}\n' +
57
+ '.badge .dot {\n' +
58
+ ' width: 6px;\n' +
59
+ ' height: 6px;\n' +
60
+ ' border-radius: 50%;\n' +
61
+ ' background: var(--green);\n' +
62
+ '}\n' +
63
+ '.spacer { flex: 1; }\n' +
64
+ '.tabs {\n' +
65
+ ' display: flex;\n' +
66
+ ' padding: 0 16px;\n' +
67
+ ' background: var(--surface);\n' +
68
+ ' border-bottom: 1px solid var(--border);\n' +
69
+ ' gap: 2px;\n' +
70
+ ' flex-shrink: 0;\n' +
71
+ '}\n' +
72
+ '.tab {\n' +
73
+ ' padding: 8px 14px;\n' +
74
+ ' font-size: 12px;\n' +
75
+ ' color: var(--text-dim);\n' +
76
+ ' cursor: pointer;\n' +
77
+ ' border-bottom: 2px solid transparent;\n' +
78
+ ' transition: all 0.15s;\n' +
79
+ ' background: none;\n' +
80
+ ' border-top: none;\n' +
81
+ ' border-left: none;\n' +
82
+ ' border-right: none;\n' +
83
+ ' font-family: inherit;\n' +
84
+ '}\n' +
85
+ '.tab:hover { color: var(--text); }\n' +
86
+ '.tab.active {\n' +
87
+ ' color: var(--accent);\n' +
88
+ ' border-bottom-color: var(--accent);\n' +
89
+ '}\n' +
90
+ '.toolbar {\n' +
91
+ ' display: flex;\n' +
92
+ ' align-items: center;\n' +
93
+ ' padding: 8px 16px;\n' +
94
+ ' gap: 10px;\n' +
95
+ ' border-bottom: 1px solid var(--border);\n' +
96
+ ' flex-shrink: 0;\n' +
97
+ '}\n' +
98
+ '.toolbar label {\n' +
99
+ ' font-size: 11px;\n' +
100
+ ' color: var(--text-dim);\n' +
101
+ ' text-transform: uppercase;\n' +
102
+ ' letter-spacing: 0.5px;\n' +
103
+ '}\n' +
104
+ '.toolbar select {\n' +
105
+ ' background: var(--surface);\n' +
106
+ ' color: var(--text);\n' +
107
+ ' border: 1px solid var(--border);\n' +
108
+ ' border-radius: 6px;\n' +
109
+ ' padding: 4px 8px;\n' +
110
+ ' font-size: 12px;\n' +
111
+ ' font-family: inherit;\n' +
112
+ ' cursor: pointer;\n' +
113
+ '}\n' +
114
+ '.toolbar select:focus { outline: none; border-color: var(--accent); }\n' +
115
+ '.toolbar input[type="text"] {\n' +
116
+ ' background: var(--surface);\n' +
117
+ ' color: var(--text);\n' +
118
+ ' border: 1px solid var(--border);\n' +
119
+ ' border-radius: 6px;\n' +
120
+ ' padding: 4px 8px;\n' +
121
+ ' font-size: 12px;\n' +
122
+ ' font-family: inherit;\n' +
123
+ ' width: 200px;\n' +
124
+ '}\n' +
125
+ '.toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }\n' +
126
+ '.toolbar input[type="text"]::placeholder { color: var(--text-dim); }\n' +
127
+ '.sep { width: 1px; height: 20px; background: var(--border); }\n' +
128
+ '.action-btn {\n' +
129
+ ' padding: 5px 12px;\n' +
130
+ ' border-radius: 6px;\n' +
131
+ ' border: 1px solid;\n' +
132
+ ' background: transparent;\n' +
133
+ ' cursor: pointer;\n' +
134
+ ' font-family: inherit;\n' +
135
+ ' font-size: 11px;\n' +
136
+ ' transition: all 0.15s ease;\n' +
137
+ ' white-space: nowrap;\n' +
138
+ '}\n' +
139
+ '.action-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n' +
140
+ '.action-btn.blue { color: var(--accent); border-color: #1f6feb44; }\n' +
141
+ '.action-btn.blue:hover:not(:disabled) { background: #1f6feb22; border-color: var(--accent); }\n' +
142
+ '.action-btn.orange { color: var(--yellow); border-color: #d2992244; }\n' +
143
+ '.action-btn.orange:hover:not(:disabled) { background: #d2992222; border-color: var(--yellow); }\n' +
144
+ '.action-btn.red { color: var(--red); border-color: #f8514944; }\n' +
145
+ '.action-btn.red:hover:not(:disabled) { background: #f8514922; border-color: var(--red); }\n' +
146
+ '.action-btn.gray { color: var(--text-dim); border-color: #8b949e44; }\n' +
147
+ '.action-btn.gray:hover:not(:disabled) { background: #8b949e22; border-color: var(--text-dim); }\n' +
148
+ '.content-area {\n' +
149
+ ' flex: 1;\n' +
150
+ ' overflow: hidden;\n' +
151
+ ' position: relative;\n' +
152
+ ' display: flex;\n' +
153
+ ' flex-direction: column;\n' +
154
+ '}\n' +
155
+ '.log-wrap {\n' +
156
+ ' flex: 1;\n' +
157
+ ' overflow: hidden;\n' +
158
+ ' position: relative;\n' +
159
+ '}\n' +
160
+ '.log-view {\n' +
161
+ ' height: 100%;\n' +
162
+ ' overflow-y: auto;\n' +
163
+ ' padding: 8px 16px;\n' +
164
+ ' font-size: 12px;\n' +
165
+ ' line-height: 1.5;\n' +
166
+ '}\n' +
167
+ '.log-line { white-space: pre-wrap; word-break: break-all; }\n' +
168
+ '.log-line .ts { color: var(--text-dim); }\n' +
169
+ '.log-line .src { display: inline-block; width: 7ch; }\n' +
170
+ '.log-line .src.zero { color: var(--purple); }\n' +
171
+ '.log-line .src.pglite { color: var(--green); }\n' +
172
+ '.log-line .src.proxy { color: var(--yellow); }\n' +
173
+ '.log-line .src.orez { color: var(--accent); }\n' +
174
+ '.log-line .src.s3 { color: #79c0ff; }\n' +
175
+ '.log-line.level-error .msg { color: var(--red); }\n' +
176
+ '.log-line.level-warn .msg { color: var(--yellow); }\n' +
177
+ '.log-line.level-info .msg { color: var(--text); }\n' +
178
+ '.log-line.level-debug .msg { color: var(--text-dim); }\n' +
179
+ '.jump-btn {\n' +
180
+ ' position: absolute;\n' +
181
+ ' bottom: 16px;\n' +
182
+ ' left: 50%;\n' +
183
+ ' transform: translateX(-50%);\n' +
184
+ ' padding: 6px 16px;\n' +
185
+ ' border-radius: 20px;\n' +
186
+ ' background: var(--accent);\n' +
187
+ ' color: #fff;\n' +
188
+ ' border: none;\n' +
189
+ ' font-size: 12px;\n' +
190
+ ' font-family: inherit;\n' +
191
+ ' cursor: pointer;\n' +
192
+ ' opacity: 0;\n' +
193
+ ' transition: opacity 0.2s;\n' +
194
+ ' pointer-events: none;\n' +
195
+ ' z-index: 10;\n' +
196
+ '}\n' +
197
+ '.jump-btn.visible { opacity: 1; pointer-events: auto; }\n' +
198
+ '.env-view {\n' +
199
+ ' height: 100%;\n' +
200
+ ' overflow-y: auto;\n' +
201
+ ' padding: 16px;\n' +
202
+ ' display: none;\n' +
203
+ '}\n' +
204
+ '.env-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
205
+ '.env-table th {\n' +
206
+ ' text-align: left;\n' +
207
+ ' padding: 6px 12px;\n' +
208
+ ' color: var(--text-dim);\n' +
209
+ ' border-bottom: 1px solid var(--border);\n' +
210
+ ' font-weight: 500;\n' +
211
+ ' text-transform: uppercase;\n' +
212
+ ' font-size: 10px;\n' +
213
+ ' letter-spacing: 0.5px;\n' +
214
+ '}\n' +
215
+ '.env-table td {\n' +
216
+ ' padding: 6px 12px;\n' +
217
+ ' border-bottom: 1px solid #21262d;\n' +
218
+ '}\n' +
219
+ '.env-table td:first-child { color: var(--accent); white-space: nowrap; }\n' +
220
+ '.env-table td:last-child { color: var(--text); word-break: break-all; }\n' +
221
+ '.env-table tr:hover td { background: #161b22; }\n' +
222
+ // http view
223
+ '.http-view {\n' +
224
+ ' height: 100%;\n' +
225
+ ' overflow-y: auto;\n' +
226
+ ' padding: 0;\n' +
227
+ ' display: none;\n' +
228
+ '}\n' +
229
+ '.http-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
230
+ '.http-table th {\n' +
231
+ ' text-align: left;\n' +
232
+ ' padding: 6px 12px;\n' +
233
+ ' color: var(--text-dim);\n' +
234
+ ' border-bottom: 1px solid var(--border);\n' +
235
+ ' font-weight: 500;\n' +
236
+ ' text-transform: uppercase;\n' +
237
+ ' font-size: 10px;\n' +
238
+ ' letter-spacing: 0.5px;\n' +
239
+ ' position: sticky;\n' +
240
+ ' top: 0;\n' +
241
+ ' background: var(--bg);\n' +
242
+ ' z-index: 1;\n' +
243
+ '}\n' +
244
+ '.http-table td {\n' +
245
+ ' padding: 5px 12px;\n' +
246
+ ' border-bottom: 1px solid #21262d;\n' +
247
+ ' white-space: nowrap;\n' +
248
+ '}\n' +
249
+ '.http-table tr.http-row { cursor: pointer; }\n' +
250
+ '.http-table tr.http-row:hover td { background: #161b22; }\n' +
251
+ '.http-table .method { font-weight: 600; }\n' +
252
+ '.http-table .method.get { color: var(--green); }\n' +
253
+ '.http-table .method.post { color: var(--yellow); }\n' +
254
+ '.http-table .method.put { color: var(--accent); }\n' +
255
+ '.http-table .method.delete { color: var(--red); }\n' +
256
+ '.http-table .method.patch { color: #79c0ff; }\n' +
257
+ '.http-table .method.ws { color: var(--purple); }\n' +
258
+ '.http-table .status.s2 { color: var(--green); }\n' +
259
+ '.http-table .status.s3 { color: var(--yellow); }\n' +
260
+ '.http-table .status.s4 { color: var(--red); }\n' +
261
+ '.http-table .status.s5 { color: var(--red); font-weight: 600; }\n' +
262
+ '.http-table .path { color: var(--text); max-width: 500px; overflow: hidden; text-overflow: ellipsis; }\n' +
263
+ '.http-table .dur { color: var(--text-dim); }\n' +
264
+ '.http-table .sz { color: var(--text-dim); }\n' +
265
+ '.http-detail {\n' +
266
+ ' display: none;\n' +
267
+ '}\n' +
268
+ '.http-detail.open { display: table-row; }\n' +
269
+ '.http-detail td {\n' +
270
+ ' padding: 8px 12px 12px 24px;\n' +
271
+ ' background: #0c0e14;\n' +
272
+ ' border-bottom: 1px solid var(--border);\n' +
273
+ '}\n' +
274
+ '.http-detail .hdr-section { margin-bottom: 8px; }\n' +
275
+ '.http-detail .hdr-title {\n' +
276
+ ' font-size: 10px;\n' +
277
+ ' text-transform: uppercase;\n' +
278
+ ' color: var(--text-dim);\n' +
279
+ ' letter-spacing: 0.5px;\n' +
280
+ ' margin-bottom: 4px;\n' +
281
+ '}\n' +
282
+ '.http-detail .hdr-line {\n' +
283
+ ' font-size: 11px;\n' +
284
+ ' line-height: 1.6;\n' +
285
+ ' white-space: pre-wrap;\n' +
286
+ ' word-break: break-all;\n' +
287
+ '}\n' +
288
+ '.http-detail .hdr-key { color: var(--accent); }\n' +
289
+ '.http-detail .hdr-val { color: var(--text-dim); }\n' +
290
+ // actions panel
291
+ '.actions-panel {\n' +
292
+ ' flex-shrink: 0;\n' +
293
+ ' border-top: 1px solid var(--border);\n' +
294
+ ' background: var(--surface);\n' +
295
+ ' padding: 8px 16px;\n' +
296
+ '}\n' +
297
+ '.action-row {\n' +
298
+ ' display: flex;\n' +
299
+ ' align-items: center;\n' +
300
+ ' gap: 8px;\n' +
301
+ ' padding: 4px 0;\n' +
302
+ '}\n' +
303
+ '.action-label {\n' +
304
+ ' font-size: 11px;\n' +
305
+ ' font-weight: 600;\n' +
306
+ ' width: 7ch;\n' +
307
+ ' flex-shrink: 0;\n' +
308
+ '}\n' +
309
+ '.action-label.zero { color: var(--purple); }\n' +
310
+ '.action-label.logs { color: var(--text-dim); }\n' +
311
+ // toast
312
+ '.toast {\n' +
313
+ ' position: fixed;\n' +
314
+ ' bottom: 20px;\n' +
315
+ ' right: 20px;\n' +
316
+ ' padding: 10px 16px;\n' +
317
+ ' border-radius: 8px;\n' +
318
+ ' background: var(--surface);\n' +
319
+ ' border: 1px solid var(--border);\n' +
320
+ ' color: var(--text);\n' +
321
+ ' font-size: 12px;\n' +
322
+ ' font-family: inherit;\n' +
323
+ ' opacity: 0;\n' +
324
+ ' transform: translateY(10px);\n' +
325
+ ' transition: all 0.3s ease;\n' +
326
+ ' pointer-events: none;\n' +
327
+ ' z-index: 100;\n' +
328
+ '}\n' +
329
+ '.toast.show { opacity: 1; transform: translateY(0); }\n' +
330
+ '.toast.error { border-color: var(--red); color: var(--red); }\n' +
331
+ '.toast.success { border-color: var(--green); color: var(--green); }\n' +
332
+ '</style>\n' +
333
+ '</head>\n' +
334
+ '<body>\n' +
335
+ ' <div class="header">\n' +
336
+ ' <span class="logo">&#9670; orez admin</span>\n' +
337
+ ' <div class="spacer"></div>\n' +
338
+ ' <span class="badge"><span class="dot"></span> pg <span id="pg-port">-</span></span>\n' +
339
+ ' <span class="badge"><span class="dot"></span> zero <span id="zero-port">-</span></span>\n' +
340
+ ' <span class="badge" id="uptime-badge">&#9201; --</span>\n' +
341
+ ' </div>\n' +
342
+ '\n' +
343
+ ' <div class="tabs" id="tab-bar">\n' +
344
+ ' <button class="tab active" data-source="">All</button>\n' +
345
+ ' <button class="tab" data-source="zero">Zero</button>\n' +
346
+ ' <button class="tab" data-source="pglite">PGlite</button>\n' +
347
+ ' <button class="tab" data-source="proxy">Proxy</button>\n' +
348
+ ' <button class="tab" data-source="orez">Orez</button>\n' +
349
+ ' <button class="tab" data-source="s3">S3</button>\n' +
350
+ ' <button class="tab" data-source="http">HTTP</button>\n' +
351
+ ' <button class="tab" data-source="env">Env</button>\n' +
352
+ ' </div>\n' +
353
+ '\n' +
354
+ ' <div class="toolbar" id="toolbar">\n' +
355
+ ' <label>Level</label>\n' +
356
+ ' <select id="level-filter">\n' +
357
+ ' <option value="" selected>all levels</option>\n' +
358
+ ' <option value="error">error only</option>\n' +
359
+ ' <option value="warn">warn+</option>\n' +
360
+ ' <option value="info">info+</option>\n' +
361
+ ' </select>\n' +
362
+ ' </div>\n' +
363
+ '\n' +
364
+ ' <div class="toolbar" id="http-toolbar" style="display:none">\n' +
365
+ ' <label>Filter</label>\n' +
366
+ ' <input type="text" id="http-path-filter" placeholder="filter by path...">\n' +
367
+ ' </div>\n' +
368
+ '\n' +
369
+ ' <div class="content-area">\n' +
370
+ ' <div class="log-wrap">\n' +
371
+ ' <div class="log-view" id="log-view"></div>\n' +
372
+ ' <div class="env-view" id="env-view">\n' +
373
+ ' <table class="env-table">\n' +
374
+ ' <thead><tr><th>Variable</th><th>Value</th></tr></thead>\n' +
375
+ ' <tbody id="env-body"></tbody>\n' +
376
+ ' </table>\n' +
377
+ ' </div>\n' +
378
+ ' <div class="http-view" id="http-view">\n' +
379
+ ' <table class="http-table">\n' +
380
+ ' <thead><tr>\n' +
381
+ ' <th>Time</th>\n' +
382
+ ' <th>Method</th>\n' +
383
+ ' <th>Path</th>\n' +
384
+ ' <th>Status</th>\n' +
385
+ ' <th>Duration</th>\n' +
386
+ ' <th>Size</th>\n' +
387
+ ' </tr></thead>\n' +
388
+ ' <tbody id="http-body"></tbody>\n' +
389
+ ' </table>\n' +
390
+ ' </div>\n' +
391
+ ' <button class="jump-btn" id="jump-btn" onclick="jumpToBottom()">&#x2193; Jump to bottom</button>\n' +
392
+ ' </div>\n' +
393
+ '\n' +
394
+ ' <div class="actions-panel" id="actions-panel">\n' +
395
+ ' <div class="action-row">\n' +
396
+ ' <span class="action-label zero">zero</span>\n' +
397
+ ' <button class="action-btn blue" data-zero-action onclick="doAction(\'restart-zero\', this)">&#x21bb; Restart</button>\n' +
398
+ ' <button class="action-btn orange" data-zero-action onclick="doAction(\'reset-zero\', this)">&#x21ba; Reset</button>\n' +
399
+ ' </div>\n' +
400
+ ' <div class="action-row">\n' +
401
+ ' <span class="action-label logs">logs</span>\n' +
402
+ ' <button class="action-btn gray" onclick="doAction(\'clear-logs\', this)">&#x2715; Clear Logs</button>\n' +
403
+ ' <button class="action-btn gray" onclick="doAction(\'clear-http\', this)">&#x2715; Clear HTTP</button>\n' +
404
+ ' </div>\n' +
405
+ ' </div>\n' +
406
+ ' </div>\n' +
407
+ '\n' +
408
+ ' <div class="toast" id="toast"></div>\n' +
409
+ '\n' +
410
+ '<script>\n' +
411
+ 'var activeSource = "";\n' +
412
+ 'var activeLevel = "";\n' +
413
+ 'var lastCursor = 0;\n' +
414
+ 'var autoScroll = true;\n' +
415
+ 'var envLoaded = false;\n' +
416
+ 'var isEnvTab = false;\n' +
417
+ 'var isHttpTab = false;\n' +
418
+ 'var httpCursor = 0;\n' +
419
+ 'var httpAutoScroll = true;\n' +
420
+ '\n' +
421
+ 'var logView = document.getElementById("log-view");\n' +
422
+ 'var envView = document.getElementById("env-view");\n' +
423
+ 'var httpView = document.getElementById("http-view");\n' +
424
+ 'var jumpBtn = document.getElementById("jump-btn");\n' +
425
+ 'var toastEl = document.getElementById("toast");\n' +
426
+ 'var toolbar = document.getElementById("toolbar");\n' +
427
+ 'var httpToolbar = document.getElementById("http-toolbar");\n' +
428
+ '\n' +
429
+ 'document.getElementById("tab-bar").addEventListener("click", function(e) {\n' +
430
+ ' var tab = e.target.closest(".tab");\n' +
431
+ ' if (!tab) return;\n' +
432
+ ' document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });\n' +
433
+ ' tab.classList.add("active");\n' +
434
+ ' var source = tab.dataset.source;\n' +
435
+ ' isEnvTab = source === "env";\n' +
436
+ ' isHttpTab = source === "http";\n' +
437
+ ' logView.style.display = "none";\n' +
438
+ ' envView.style.display = "none";\n' +
439
+ ' httpView.style.display = "none";\n' +
440
+ ' toolbar.style.display = "none";\n' +
441
+ ' httpToolbar.style.display = "none";\n' +
442
+ ' if (isEnvTab) {\n' +
443
+ ' envView.style.display = "block";\n' +
444
+ ' if (!envLoaded) loadEnv();\n' +
445
+ ' } else if (isHttpTab) {\n' +
446
+ ' httpView.style.display = "block";\n' +
447
+ ' httpToolbar.style.display = "flex";\n' +
448
+ ' httpCursor = 0;\n' +
449
+ ' document.getElementById("http-body").innerHTML = "";\n' +
450
+ ' fetchHttp();\n' +
451
+ ' } else {\n' +
452
+ ' logView.style.display = "block";\n' +
453
+ ' toolbar.style.display = "flex";\n' +
454
+ ' activeSource = source;\n' +
455
+ ' lastCursor = 0;\n' +
456
+ ' logView.innerHTML = "";\n' +
457
+ ' fetchLogs();\n' +
458
+ ' }\n' +
459
+ '});\n' +
460
+ '\n' +
461
+ 'document.getElementById("level-filter").addEventListener("change", function(e) {\n' +
462
+ ' activeLevel = e.target.value;\n' +
463
+ ' lastCursor = 0;\n' +
464
+ ' logView.innerHTML = "";\n' +
465
+ ' fetchLogs();\n' +
466
+ '});\n' +
467
+ '\n' +
468
+ 'var httpFilterTimeout = null;\n' +
469
+ 'document.getElementById("http-path-filter").addEventListener("input", function() {\n' +
470
+ ' clearTimeout(httpFilterTimeout);\n' +
471
+ ' httpFilterTimeout = setTimeout(function() {\n' +
472
+ ' httpCursor = 0;\n' +
473
+ ' document.getElementById("http-body").innerHTML = "";\n' +
474
+ ' fetchHttp();\n' +
475
+ ' }, 300);\n' +
476
+ '});\n' +
477
+ '\n' +
478
+ 'logView.addEventListener("scroll", function() {\n' +
479
+ ' var atBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 40;\n' +
480
+ ' autoScroll = atBottom;\n' +
481
+ ' jumpBtn.classList.toggle("visible", !atBottom);\n' +
482
+ '});\n' +
483
+ '\n' +
484
+ 'httpView.addEventListener("scroll", function() {\n' +
485
+ ' var atBottom = httpView.scrollHeight - httpView.scrollTop - httpView.clientHeight < 40;\n' +
486
+ ' httpAutoScroll = atBottom;\n' +
487
+ '});\n' +
488
+ '\n' +
489
+ 'function jumpToBottom() {\n' +
490
+ ' var el = isHttpTab ? httpView : logView;\n' +
491
+ ' el.scrollTop = el.scrollHeight;\n' +
492
+ ' autoScroll = true;\n' +
493
+ ' httpAutoScroll = true;\n' +
494
+ ' jumpBtn.classList.remove("visible");\n' +
495
+ '}\n' +
496
+ '\n' +
497
+ 'function fmtTime(ts) {\n' +
498
+ ' var d = new Date(ts);\n' +
499
+ ' return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })\n' +
500
+ ' + "." + String(d.getMilliseconds()).padStart(3, "0");\n' +
501
+ '}\n' +
502
+ '\n' +
503
+ 'function escHtml(s) {\n' +
504
+ ' return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");\n' +
505
+ '}\n' +
506
+ '\n' +
507
+ 'function fmtSize(bytes) {\n' +
508
+ ' if (bytes === 0) return "-";\n' +
509
+ ' if (bytes < 1024) return bytes + "B";\n' +
510
+ ' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "kb";\n' +
511
+ ' return (bytes / (1024 * 1024)).toFixed(1) + "MB";\n' +
512
+ '}\n' +
513
+ '\n' +
514
+ 'function renderEntries(entries) {\n' +
515
+ ' var frag = document.createDocumentFragment();\n' +
516
+ ' for (var i = 0; i < entries.length; i++) {\n' +
517
+ ' var e = entries[i];\n' +
518
+ ' var div = document.createElement("div");\n' +
519
+ ' div.className = "log-line level-" + e.level;\n' +
520
+ ' div.innerHTML = \'<span class="ts">\' + fmtTime(e.ts) + "</span> "\n' +
521
+ ' + \'<span class="src \' + e.source + \'">\' + e.source.padEnd(6) + "</span> "\n' +
522
+ ' + \'<span class="msg">\' + escHtml(e.msg) + "</span>";\n' +
523
+ ' frag.appendChild(div);\n' +
524
+ ' }\n' +
525
+ ' logView.appendChild(frag);\n' +
526
+ ' if (autoScroll) logView.scrollTop = logView.scrollHeight;\n' +
527
+ '}\n' +
528
+ '\n' +
529
+ 'function renderHttpEntries(entries) {\n' +
530
+ ' var tbody = document.getElementById("http-body");\n' +
531
+ ' var frag = document.createDocumentFragment();\n' +
532
+ ' for (var i = 0; i < entries.length; i++) {\n' +
533
+ ' var e = entries[i];\n' +
534
+ ' var tr = document.createElement("tr");\n' +
535
+ ' tr.className = "http-row";\n' +
536
+ ' tr.dataset.id = e.id;\n' +
537
+ ' var mc = e.method.toLowerCase();\n' +
538
+ ' var sc = "s" + String(e.status).charAt(0);\n' +
539
+ ' tr.innerHTML = "<td>" + fmtTime(e.ts) + "</td>"\n' +
540
+ ' + \'<td><span class="method \' + mc + \'">\' + e.method + "</span></td>"\n' +
541
+ ' + \'<td class="path">\' + escHtml(e.path) + "</td>"\n' +
542
+ ' + \'<td><span class="status \' + sc + \'">\' + e.status + "</span></td>"\n' +
543
+ ' + \'<td class="dur">\' + e.duration + "ms</td>"\n' +
544
+ ' + \'<td class="sz">\' + fmtSize(e.resSize) + "</td>";\n' +
545
+ ' tr.addEventListener("click", (function(entry) {\n' +
546
+ ' return function() { toggleHttpDetail(this, entry); };\n' +
547
+ ' })(e));\n' +
548
+ ' frag.appendChild(tr);\n' +
549
+ ' }\n' +
550
+ ' tbody.appendChild(frag);\n' +
551
+ ' if (httpAutoScroll) httpView.scrollTop = httpView.scrollHeight;\n' +
552
+ '}\n' +
553
+ '\n' +
554
+ 'function toggleHttpDetail(row, entry) {\n' +
555
+ ' var next = row.nextElementSibling;\n' +
556
+ ' if (next && next.classList.contains("http-detail")) {\n' +
557
+ ' next.classList.toggle("open");\n' +
558
+ ' return;\n' +
559
+ ' }\n' +
560
+ ' var detail = document.createElement("tr");\n' +
561
+ ' detail.className = "http-detail open";\n' +
562
+ ' var html = \'<td colspan="6">\';\n' +
563
+ ' html += \'<div class="hdr-section"><div class="hdr-title">request headers</div>\';\n' +
564
+ ' var rk = Object.keys(entry.reqHeaders || {}).sort();\n' +
565
+ ' for (var i = 0; i < rk.length; i++) {\n' +
566
+ ' html += \'<div class="hdr-line"><span class="hdr-key">\' + escHtml(rk[i]) + \'</span>: <span class="hdr-val">\' + escHtml(entry.reqHeaders[rk[i]]) + "</span></div>";\n' +
567
+ ' }\n' +
568
+ ' html += "</div>";\n' +
569
+ ' html += \'<div class="hdr-section"><div class="hdr-title">response headers</div>\';\n' +
570
+ ' var sk = Object.keys(entry.resHeaders || {}).sort();\n' +
571
+ ' for (var j = 0; j < sk.length; j++) {\n' +
572
+ ' html += \'<div class="hdr-line"><span class="hdr-key">\' + escHtml(sk[j]) + \'</span>: <span class="hdr-val">\' + escHtml(entry.resHeaders[sk[j]]) + "</span></div>";\n' +
573
+ ' }\n' +
574
+ ' html += "</div>";\n' +
575
+ ' if (entry.reqSize > 0) html += \'<div class="hdr-line"><span class="hdr-key">request body size</span>: <span class="hdr-val">\' + fmtSize(entry.reqSize) + "</span></div>";\n' +
576
+ ' html += "</td>";\n' +
577
+ ' detail.innerHTML = html;\n' +
578
+ ' row.parentNode.insertBefore(detail, row.nextSibling);\n' +
579
+ '}\n' +
580
+ '\n' +
581
+ 'function fetchLogs() {\n' +
582
+ ' var params = new URLSearchParams();\n' +
583
+ ' if (activeSource) params.set("source", activeSource);\n' +
584
+ ' if (activeLevel) params.set("level", activeLevel);\n' +
585
+ ' if (lastCursor) params.set("since", String(lastCursor));\n' +
586
+ ' fetch("/api/logs?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
587
+ ' if (data.entries && data.entries.length > 0) renderEntries(data.entries);\n' +
588
+ ' if (data.cursor) lastCursor = data.cursor;\n' +
589
+ ' }).catch(function() {});\n' +
590
+ '}\n' +
591
+ '\n' +
592
+ 'function fetchHttp() {\n' +
593
+ ' var params = new URLSearchParams();\n' +
594
+ ' if (httpCursor) params.set("since", String(httpCursor));\n' +
595
+ ' var pathFilter = document.getElementById("http-path-filter").value;\n' +
596
+ ' if (pathFilter) params.set("path", pathFilter);\n' +
597
+ ' fetch("/api/http-log?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
598
+ ' if (data.entries && data.entries.length > 0) renderHttpEntries(data.entries);\n' +
599
+ ' if (data.cursor) httpCursor = data.cursor;\n' +
600
+ ' }).catch(function() {});\n' +
601
+ '}\n' +
602
+ '\n' +
603
+ 'function loadEnv() {\n' +
604
+ ' fetch("/api/env").then(function(res) { return res.json(); }).then(function(data) {\n' +
605
+ ' var tbody = document.getElementById("env-body");\n' +
606
+ ' tbody.innerHTML = "";\n' +
607
+ ' var keys = Object.keys(data.env).sort();\n' +
608
+ ' for (var i = 0; i < keys.length; i++) {\n' +
609
+ ' var tr = document.createElement("tr");\n' +
610
+ ' tr.innerHTML = "<td>" + escHtml(keys[i]) + "</td><td>" + escHtml(String(data.env[keys[i]])) + "</td>";\n' +
611
+ ' tbody.appendChild(tr);\n' +
612
+ ' }\n' +
613
+ ' envLoaded = true;\n' +
614
+ ' }).catch(function() {});\n' +
615
+ '}\n' +
616
+ '\n' +
617
+ 'function fetchStatus() {\n' +
618
+ ' fetch("/api/status").then(function(res) { return res.json(); }).then(function(data) {\n' +
619
+ ' document.getElementById("pg-port").textContent = ":" + data.pgPort;\n' +
620
+ ' document.getElementById("zero-port").textContent = ":" + data.zeroPort;\n' +
621
+ ' var m = Math.floor(data.uptime / 60);\n' +
622
+ ' var s = data.uptime % 60;\n' +
623
+ ' document.getElementById("uptime-badge").textContent = "\\u23F1 " + (m > 0 ? m + "m " : "") + s + "s";\n' +
624
+ ' var zeroDisabled = data.skipZeroCache;\n' +
625
+ ' document.querySelectorAll("[data-zero-action]").forEach(function(btn) {\n' +
626
+ ' btn.disabled = zeroDisabled;\n' +
627
+ ' });\n' +
628
+ ' }).catch(function() {});\n' +
629
+ '}\n' +
630
+ '\n' +
631
+ 'function doAction(action, btn) {\n' +
632
+ ' if (action === "reset-zero") {\n' +
633
+ ' if (!confirm("Reset zero-cache? This deletes the replica and resyncs from scratch.")) return;\n' +
634
+ ' }\n' +
635
+ ' btn.disabled = true;\n' +
636
+ ' var origText = btn.textContent;\n' +
637
+ ' btn.textContent = "...";\n' +
638
+ ' fetch("/api/actions/" + action, { method: "POST" })\n' +
639
+ ' .then(function(res) { return res.json(); })\n' +
640
+ ' .then(function(data) {\n' +
641
+ ' showToast(data.message || "done", data.ok ? "success" : "error");\n' +
642
+ ' if (action === "clear-logs") {\n' +
643
+ ' logView.innerHTML = "";\n' +
644
+ ' lastCursor = 0;\n' +
645
+ ' }\n' +
646
+ ' if (action === "clear-http") {\n' +
647
+ ' document.getElementById("http-body").innerHTML = "";\n' +
648
+ ' httpCursor = 0;\n' +
649
+ ' }\n' +
650
+ ' })\n' +
651
+ ' .catch(function(err) {\n' +
652
+ ' showToast("failed: " + err.message, "error");\n' +
653
+ ' })\n' +
654
+ ' .finally(function() {\n' +
655
+ ' btn.disabled = false;\n' +
656
+ ' btn.textContent = origText;\n' +
657
+ ' });\n' +
658
+ '}\n' +
659
+ '\n' +
660
+ 'function showToast(msg, type) {\n' +
661
+ ' toastEl.textContent = msg;\n' +
662
+ ' toastEl.className = "toast " + type + " show";\n' +
663
+ ' setTimeout(function() { toastEl.className = "toast"; }, 2500);\n' +
664
+ '}\n' +
665
+ '\n' +
666
+ 'fetchLogs();\n' +
667
+ 'fetchStatus();\n' +
668
+ 'setInterval(function() {\n' +
669
+ ' if (document.hidden) return;\n' +
670
+ ' if (isHttpTab) fetchHttp();\n' +
671
+ ' else if (!isEnvTab) fetchLogs();\n' +
672
+ '}, 1000);\n' +
673
+ 'setInterval(function() { if (!document.hidden) fetchStatus(); }, 5000);\n' +
674
+ 'document.addEventListener("visibilitychange", function() {\n' +
675
+ ' if (document.hidden) return;\n' +
676
+ ' if (isHttpTab) fetchHttp();\n' +
677
+ ' else if (!isEnvTab) fetchLogs();\n' +
678
+ ' fetchStatus();\n' +
679
+ '});\n' +
680
+ '</script>\n' +
681
+ '</body>\n' +
682
+ '</html>'
683
+ )
682
684
  }