orez 0.2.10 → 0.2.11

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/dist/admin/ui.js CHANGED
@@ -1,728 +1,1323 @@
1
1
  export function getAdminHtml() {
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: #000;\n' +
11
- ' --surface: #0a0a0a;\n' +
12
- ' --border: #222;\n' +
13
- ' --text: #fff;\n' +
14
- ' --text-dim: #666;\n' +
15
- ' --accent: #fff;\n' +
16
- ' --green: #888;\n' +
17
- ' --yellow: #999;\n' +
18
- ' --red: #f55;\n' +
19
- ' --purple: #aaa;\n' +
20
- '}\n' +
21
- '* { margin: 0; padding: 0; box-sizing: border-box; }\n' +
22
- '::-webkit-scrollbar { width: 8px; height: 8px; }\n' +
23
- '::-webkit-scrollbar-track { background: var(--bg); }\n' +
24
- '::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }\n' +
25
- '::-webkit-scrollbar-thumb:hover { background: #555; }\n' +
26
- 'body {\n' +
27
- ' font-family: -apple-system, BlinkMacSystemFont, "SF Pro", system-ui, sans-serif;\n' +
28
- ' background: var(--bg);\n' +
29
- ' color: var(--text);\n' +
30
- ' height: 100vh;\n' +
31
- ' display: flex;\n' +
32
- ' flex-direction: column;\n' +
33
- ' overflow: hidden;\n' +
34
- '}\n' +
35
- '.header {\n' +
36
- ' display: flex;\n' +
37
- ' align-items: center;\n' +
38
- ' padding: 4px 12px;\n' +
39
- ' background: var(--surface);\n' +
40
- ' border-bottom: 0.5px solid var(--border);\n' +
41
- ' gap: 8px;\n' +
42
- ' flex-shrink: 0;\n' +
43
- '}\n' +
44
- '.header .logo {\n' +
45
- ' font-size: 12px;\n' +
46
- ' font-weight: 700;\n' +
47
- ' color: var(--accent);\n' +
48
- ' letter-spacing: -0.5px;\n' +
49
- '}\n' +
50
- '.badge {\n' +
51
- ' display: inline-flex;\n' +
52
- ' align-items: center;\n' +
53
- ' padding: 1px 6px;\n' +
54
- ' border-radius: 12px;\n' +
55
- ' font-size: 10px;\n' +
56
- ' border: 0.5px solid var(--border);\n' +
57
- ' color: var(--text-dim);\n' +
58
- ' gap: 3px;\n' +
59
- '}\n' +
60
- '.badge .dot {\n' +
61
- ' width: 6px;\n' +
62
- ' height: 6px;\n' +
63
- ' border-radius: 50%;\n' +
64
- ' background: var(--green);\n' +
65
- '}\n' +
66
- '.spacer { flex: 1; }\n' +
67
- '.tabs {\n' +
68
- ' display: flex;\n' +
69
- ' padding: 0 12px;\n' +
70
- ' background: var(--surface);\n' +
71
- ' border-bottom: 0.5px solid var(--border);\n' +
72
- ' gap: 0;\n' +
73
- ' flex-shrink: 0;\n' +
74
- '}\n' +
75
- '.tab {\n' +
76
- ' padding: 4px 10px;\n' +
77
- ' font-size: 11px;\n' +
78
- ' color: var(--text-dim);\n' +
79
- ' cursor: pointer;\n' +
80
- ' border-bottom: 2px solid transparent;\n' +
81
- ' transition: all 0.15s;\n' +
82
- ' background: none;\n' +
83
- ' border-top: none;\n' +
84
- ' border-left: none;\n' +
85
- ' border-right: none;\n' +
86
- ' font-family: inherit;\n' +
87
- '}\n' +
88
- '.tab:hover { color: var(--text); }\n' +
89
- '.tab.active {\n' +
90
- ' color: var(--accent);\n' +
91
- ' border-bottom-color: var(--accent);\n' +
92
- '}\n' +
93
- '.toolbar {\n' +
94
- ' display: flex;\n' +
95
- ' align-items: center;\n' +
96
- ' padding: 3px 12px;\n' +
97
- ' gap: 8px;\n' +
98
- ' border-bottom: 0.5px solid var(--border);\n' +
99
- ' flex-shrink: 0;\n' +
100
- '}\n' +
101
- '.toolbar label {\n' +
102
- ' font-size: 10px;\n' +
103
- ' color: var(--text-dim);\n' +
104
- ' text-transform: uppercase;\n' +
105
- ' letter-spacing: 0.5px;\n' +
106
- '}\n' +
107
- '.toolbar select {\n' +
108
- ' background: var(--surface);\n' +
109
- ' color: var(--text);\n' +
110
- ' border: 0.5px solid var(--border);\n' +
111
- ' border-radius: 4px;\n' +
112
- ' padding: 2px 6px;\n' +
113
- ' font-size: 11px;\n' +
114
- ' font-family: inherit;\n' +
115
- ' cursor: pointer;\n' +
116
- '}\n' +
117
- '.toolbar select:focus { outline: none; border-color: var(--accent); }\n' +
118
- '.toolbar input[type="text"] {\n' +
119
- ' background: var(--surface);\n' +
120
- ' color: var(--text);\n' +
121
- ' border: 0.5px solid var(--border);\n' +
122
- ' border-radius: 4px;\n' +
123
- ' padding: 2px 6px;\n' +
124
- ' font-size: 11px;\n' +
125
- ' font-family: inherit;\n' +
126
- ' width: 180px;\n' +
127
- '}\n' +
128
- '.toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }\n' +
129
- '.toolbar input[type="text"]::placeholder { color: var(--text-dim); }\n' +
130
- '.sep { width: 1px; height: 20px; background: var(--border); }\n' +
131
- '.action-btn {\n' +
132
- ' padding: 2px 8px;\n' +
133
- ' border-radius: 4px;\n' +
134
- ' border: 1px solid;\n' +
135
- ' background: transparent;\n' +
136
- ' cursor: pointer;\n' +
137
- ' font-family: inherit;\n' +
138
- ' font-size: 10px;\n' +
139
- ' transition: all 0.15s ease;\n' +
140
- ' white-space: nowrap;\n' +
141
- '}\n' +
142
- '.action-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n' +
143
- '.action-btn.blue { color: var(--accent); border-color: #ffffff22; }\n' +
144
- '.action-btn.blue:hover:not(:disabled) { background: #ffffff11; border-color: var(--accent); }\n' +
145
- '.action-btn.orange { color: var(--yellow); border-color: #ffffff22; }\n' +
146
- '.action-btn.orange:hover:not(:disabled) { background: #ffffff11; border-color: var(--yellow); }\n' +
147
- '.action-btn.red { color: var(--red); border-color: #ff555522; }\n' +
148
- '.action-btn.red:hover:not(:disabled) { background: #ff555511; border-color: var(--red); }\n' +
149
- '.action-btn.gray { color: var(--text-dim); border-color: #ffffff22; }\n' +
150
- '.action-btn.gray:hover:not(:disabled) { background: #ffffff11; border-color: var(--text-dim); }\n' +
151
- '.content-area {\n' +
152
- ' flex: 1;\n' +
153
- ' overflow: hidden;\n' +
154
- ' position: relative;\n' +
155
- ' display: flex;\n' +
156
- ' flex-direction: column;\n' +
157
- '}\n' +
158
- '.log-wrap {\n' +
159
- ' flex: 1;\n' +
160
- ' overflow: hidden;\n' +
161
- ' position: relative;\n' +
162
- '}\n' +
163
- '.log-view {\n' +
164
- ' height: 100%;\n' +
165
- ' overflow-y: auto;\n' +
166
- ' padding: 4px 12px;\n' +
167
- ' font-size: 11px;\n' +
168
- ' line-height: 1.4;\n' +
169
- '}\n' +
170
- '.log-line { white-space: pre-wrap; word-break: break-all; }\n' +
171
- '.log-line .ts { color: var(--text-dim); }\n' +
172
- '.log-line .src { display: inline-block; width: 7ch; }\n' +
173
- '.log-line .src.zero { color: var(--purple); }\n' +
174
- '.log-line .src.pglite { color: var(--green); }\n' +
175
- '.log-line .src.proxy { color: var(--yellow); }\n' +
176
- '.log-line .src.orez { color: var(--accent); }\n' +
177
- '.log-line .src.s3 { color: #888; }\n' +
178
- '.log-line.level-error .msg { color: var(--red); }\n' +
179
- '.log-line.level-warn .msg { color: var(--yellow); }\n' +
180
- '.log-line.level-info .msg { color: var(--text); }\n' +
181
- '.log-line.level-debug .msg { color: var(--text-dim); }\n' +
182
- '.jump-btn {\n' +
183
- ' position: absolute;\n' +
184
- ' bottom: 16px;\n' +
185
- ' left: 50%;\n' +
186
- ' transform: translateX(-50%);\n' +
187
- ' padding: 6px 16px;\n' +
188
- ' border-radius: 20px;\n' +
189
- ' background: #333;\n' +
190
- ' color: var(--text);\n' +
191
- ' border: 1px solid var(--border);\n' +
192
- ' font-size: 12px;\n' +
193
- ' font-family: inherit;\n' +
194
- ' cursor: pointer;\n' +
195
- ' opacity: 0;\n' +
196
- ' transition: opacity 0.2s;\n' +
197
- ' pointer-events: none;\n' +
198
- ' z-index: 10;\n' +
199
- '}\n' +
200
- '.jump-btn.visible { opacity: 1; pointer-events: auto; }\n' +
201
- '.env-view {\n' +
202
- ' height: 100%;\n' +
203
- ' overflow-y: auto;\n' +
204
- ' padding: 16px;\n' +
205
- ' display: none;\n' +
206
- '}\n' +
207
- '.env-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
208
- '.env-table th {\n' +
209
- ' text-align: left;\n' +
210
- ' padding: 6px 12px;\n' +
211
- ' color: var(--text-dim);\n' +
212
- ' border-bottom: 0.5px solid var(--border);\n' +
213
- ' font-weight: 500;\n' +
214
- ' text-transform: uppercase;\n' +
215
- ' font-size: 10px;\n' +
216
- ' letter-spacing: 0.5px;\n' +
217
- '}\n' +
218
- '.env-table td {\n' +
219
- ' padding: 6px 12px;\n' +
220
- ' border-bottom: 0.5px solid var(--border);\n' +
221
- '}\n' +
222
- '.env-table td:first-child { color: var(--accent); white-space: nowrap; }\n' +
223
- '.env-table td:last-child { color: var(--text); word-break: break-all; }\n' +
224
- '.env-table tr:hover td { background: #111; }\n' +
225
- // http view
226
- '.http-view {\n' +
227
- ' height: 100%;\n' +
228
- ' overflow-y: auto;\n' +
229
- ' padding: 0;\n' +
230
- ' display: none;\n' +
231
- '}\n' +
232
- '.http-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n' +
233
- '.http-table th {\n' +
234
- ' text-align: left;\n' +
235
- ' padding: 6px 12px;\n' +
236
- ' color: var(--text-dim);\n' +
237
- ' border-bottom: 0.5px solid var(--border);\n' +
238
- ' font-weight: 500;\n' +
239
- ' text-transform: uppercase;\n' +
240
- ' font-size: 10px;\n' +
241
- ' letter-spacing: 0.5px;\n' +
242
- ' position: sticky;\n' +
243
- ' top: 0;\n' +
244
- ' background: var(--bg);\n' +
245
- ' z-index: 1;\n' +
246
- '}\n' +
247
- '.http-table td {\n' +
248
- ' padding: 5px 12px;\n' +
249
- ' border-bottom: 0.5px solid var(--border);\n' +
250
- ' white-space: nowrap;\n' +
251
- '}\n' +
252
- '.http-table tr.http-row { cursor: pointer; }\n' +
253
- '.http-table tr.http-row:hover td { background: #111; }\n' +
254
- '.http-table .method { font-weight: 600; }\n' +
255
- '.http-table .method.get { color: var(--green); }\n' +
256
- '.http-table .method.post { color: var(--yellow); }\n' +
257
- '.http-table .method.put { color: var(--accent); }\n' +
258
- '.http-table .method.delete { color: var(--red); }\n' +
259
- '.http-table .method.patch { color: #888; }\n' +
260
- '.http-table .method.ws { color: var(--purple); }\n' +
261
- '.http-table .status.s2 { color: var(--green); }\n' +
262
- '.http-table .status.s3 { color: var(--yellow); }\n' +
263
- '.http-table .status.s4 { color: var(--red); }\n' +
264
- '.http-table .status.s5 { color: var(--red); font-weight: 600; }\n' +
265
- '.http-table .path { color: var(--text); max-width: 500px; overflow: hidden; text-overflow: ellipsis; }\n' +
266
- '.http-table .dur { color: var(--text-dim); }\n' +
267
- '.http-table .sz { color: var(--text-dim); }\n' +
268
- '.http-detail {\n' +
269
- ' display: none;\n' +
270
- '}\n' +
271
- '.http-detail.open { display: table-row; }\n' +
272
- '.http-detail td {\n' +
273
- ' padding: 8px 12px 12px 24px;\n' +
274
- ' background: #080808;\n' +
275
- ' border-bottom: 0.5px solid var(--border);\n' +
276
- '}\n' +
277
- '.http-detail .hdr-section { margin-bottom: 8px; }\n' +
278
- '.http-detail .hdr-title {\n' +
279
- ' font-size: 10px;\n' +
280
- ' text-transform: uppercase;\n' +
281
- ' color: var(--text-dim);\n' +
282
- ' letter-spacing: 0.5px;\n' +
283
- ' margin-bottom: 4px;\n' +
284
- '}\n' +
285
- '.http-detail .hdr-line {\n' +
286
- ' font-size: 11px;\n' +
287
- ' line-height: 1.6;\n' +
288
- ' white-space: pre-wrap;\n' +
289
- ' word-break: break-all;\n' +
290
- '}\n' +
291
- '.http-detail .hdr-key { color: var(--accent); }\n' +
292
- '.http-detail .hdr-val { color: var(--text-dim); }\n' +
293
- '.toolbar-actions {\n' +
294
- ' display: flex;\n' +
295
- ' align-items: center;\n' +
296
- ' gap: 6px;\n' +
297
- ' margin-left: auto;\n' +
298
- '}\n' +
299
- // toast
300
- '.toast {\n' +
301
- ' position: fixed;\n' +
302
- ' bottom: 20px;\n' +
303
- ' right: 20px;\n' +
304
- ' padding: 10px 16px;\n' +
305
- ' border-radius: 8px;\n' +
306
- ' background: var(--surface);\n' +
307
- ' border: 0.5px solid var(--border);\n' +
308
- ' color: var(--text);\n' +
309
- ' font-size: 12px;\n' +
310
- ' font-family: inherit;\n' +
311
- ' opacity: 0;\n' +
312
- ' transform: translateY(10px);\n' +
313
- ' transition: all 0.3s ease;\n' +
314
- ' pointer-events: none;\n' +
315
- ' z-index: 100;\n' +
316
- '}\n' +
317
- '.toast.show { opacity: 1; transform: translateY(0); }\n' +
318
- '.toast.error { border-color: var(--red); color: var(--red); }\n' +
319
- '.toast.success { border-color: var(--green); color: var(--green); }\n' +
320
- '</style>\n' +
321
- '</head>\n' +
322
- '<body>\n' +
323
- ' <div class="header" id="admin-header">\n' +
324
- ' <span class="logo">&#9670; oreZ admin</span>\n' +
325
- ' <div class="spacer"></div>\n' +
326
- ' <span class="badge"><span class="dot"></span> pg <span id="pg-port">-</span></span>\n' +
327
- ' <span class="badge"><span class="dot"></span> zero <span id="zero-port">-</span></span>\n' +
328
- ' <span class="badge" id="sqlite-badge">sqlite: --</span>\n' +
329
- ' <span class="badge" id="uptime-badge">&#9201; --</span>\n' +
330
- ' </div>\n' +
331
- '\n' +
332
- ' <div class="tabs" id="tab-bar">\n' +
333
- ' <button class="tab active" data-source="">All</button>\n' +
334
- ' <button class="tab" data-source="zero">Zero</button>\n' +
335
- ' <button class="tab" data-source="pglite">PGlite</button>\n' +
336
- ' <button class="tab" data-source="proxy">Proxy</button>\n' +
337
- ' <button class="tab" data-source="orez">Orez</button>\n' +
338
- ' <button class="tab" data-source="s3">S3</button>\n' +
339
- ' <button class="tab" data-source="http">HTTP</button>\n' +
340
- ' <button class="tab" data-source="env">Env</button>\n' +
341
- ' </div>\n' +
342
- '\n' +
343
- ' <div class="toolbar" id="toolbar">\n' +
344
- ' <label>Level</label>\n' +
345
- ' <select id="level-filter">\n' +
346
- ' <option value="">all levels</option>\n' +
347
- ' <option value="error">error only</option>\n' +
348
- ' <option value="warn">warn+</option>\n' +
349
- ' <option value="info">info+</option>\n' +
350
- ' <option value="debug">debug+</option>\n' +
351
- ' </select>\n' +
352
- ' <div class="toolbar-actions" id="toolbar-log-actions">\n' +
353
- ' <button class="action-btn gray" onclick="doAction(\'clear-logs\', this)">&#x2715; Clear</button>\n' +
354
- ' </div>\n' +
355
- ' </div>\n' +
356
- '\n' +
357
- ' <div class="toolbar" id="zero-toolbar" style="display:none">\n' +
358
- ' <label>Level</label>\n' +
359
- ' <select id="zero-level-filter">\n' +
360
- ' <option value="">all levels</option>\n' +
361
- ' <option value="error">error only</option>\n' +
362
- ' <option value="warn">warn+</option>\n' +
363
- ' <option value="info" selected>info+</option>\n' +
364
- ' <option value="debug">debug+</option>\n' +
365
- ' </select>\n' +
366
- ' <div class="toolbar-actions">\n' +
367
- ' <button class="action-btn blue" data-zero-action onclick="doAction(\'restart-zero\', this)">&#x21bb; Restart</button>\n' +
368
- ' <button class="action-btn orange" data-zero-action onclick="doAction(\'reset-zero\', this)">&#x21ba; Reset</button>\n' +
369
- ' <button class="action-btn red" data-zero-action onclick="doAction(\'reset-zero-full\', this)">&#x26a0; Full</button>\n' +
370
- ' <button class="action-btn gray" onclick="doAction(\'clear-logs\', this)">&#x2715; Clear</button>\n' +
371
- ' </div>\n' +
372
- ' </div>\n' +
373
- '\n' +
374
- ' <div class="toolbar" id="http-toolbar" style="display:none">\n' +
375
- ' <label>Filter</label>\n' +
376
- ' <input type="text" id="http-path-filter" placeholder="filter by path...">\n' +
377
- ' <div class="toolbar-actions">\n' +
378
- ' <button class="action-btn gray" onclick="doAction(\'clear-http\', this)">&#x2715; Clear</button>\n' +
379
- ' </div>\n' +
380
- ' </div>\n' +
381
- '\n' +
382
- ' <div class="content-area">\n' +
383
- ' <div class="log-wrap">\n' +
384
- ' <div class="log-view" id="log-view"></div>\n' +
385
- ' <div class="env-view" id="env-view">\n' +
386
- ' <table class="env-table">\n' +
387
- ' <thead><tr><th>Variable</th><th>Value</th></tr></thead>\n' +
388
- ' <tbody id="env-body"></tbody>\n' +
389
- ' </table>\n' +
390
- ' </div>\n' +
391
- ' <div class="http-view" id="http-view">\n' +
392
- ' <table class="http-table">\n' +
393
- ' <thead><tr>\n' +
394
- ' <th>Time</th>\n' +
395
- ' <th>Method</th>\n' +
396
- ' <th>Path</th>\n' +
397
- ' <th>Status</th>\n' +
398
- ' <th>Duration</th>\n' +
399
- ' <th>Size</th>\n' +
400
- ' </tr></thead>\n' +
401
- ' <tbody id="http-body"></tbody>\n' +
402
- ' </table>\n' +
403
- ' </div>\n' +
404
- ' <button class="jump-btn" id="jump-btn" onclick="jumpToBottom()">&#x2193; Jump to bottom</button>\n' +
405
- ' </div>\n' +
406
- ' </div>\n' +
407
- '\n' +
408
- ' <div class="toast" id="toast"></div>\n' +
409
- '\n' +
410
- '<script>\n' +
411
- '// resolve initial tab from url path\n' +
412
- 'var pathMap = {"/":"","/all":"","/zero":"zero","/pglite":"pglite","/proxy":"proxy","/orez":"orez","/s3":"s3","/http":"http","/env":"env"};\n' +
413
- 'var initPath = window.location.pathname.replace(/\\/$/, "") || "/";\n' +
414
- 'var initSource = pathMap[initPath] !== undefined ? pathMap[initPath] : "";\n' +
415
- 'var standalone = initPath !== "/" && initPath !== "/all";\n' +
416
- 'var activeSource = initSource;\n' +
417
- 'var activeLevel = initSource === "zero" ? "info" : "";\n' +
418
- 'var lastCursor = 0;\n' +
419
- 'var autoScroll = true;\n' +
420
- 'var envLoaded = false;\n' +
421
- 'var isEnvTab = initSource === "env";\n' +
422
- 'var isHttpTab = initSource === "http";\n' +
423
- 'var httpCursor = 0;\n' +
424
- 'var httpAutoScroll = true;\n' +
425
- '\n' +
426
- 'var logView = document.getElementById("log-view");\n' +
427
- 'var envView = document.getElementById("env-view");\n' +
428
- 'var httpView = document.getElementById("http-view");\n' +
429
- 'var jumpBtn = document.getElementById("jump-btn");\n' +
430
- 'var toastEl = document.getElementById("toast");\n' +
431
- 'var toolbar = document.getElementById("toolbar");\n' +
432
- 'var zeroToolbar = document.getElementById("zero-toolbar");\n' +
433
- 'var httpToolbar = document.getElementById("http-toolbar");\n' +
434
- '\n' +
435
- 'function sourceToPath(s) { return s ? "/" + s : "/"; }\n' +
436
- '\n' +
437
- 'function switchTab(source, pushState) {\n' +
438
- ' isEnvTab = source === "env";\n' +
439
- ' isHttpTab = source === "http";\n' +
440
- ' var isZero = source === "zero";\n' +
441
- ' if (pushState) history.pushState(null, "", sourceToPath(source));\n' +
442
- ' logView.style.display = "none";\n' +
443
- ' envView.style.display = "none";\n' +
444
- ' httpView.style.display = "none";\n' +
445
- ' toolbar.style.display = "none";\n' +
446
- ' zeroToolbar.style.display = "none";\n' +
447
- ' httpToolbar.style.display = "none";\n' +
448
- ' if (isEnvTab) {\n' +
449
- ' envView.style.display = "block";\n' +
450
- ' if (!envLoaded) loadEnv();\n' +
451
- ' } else if (isHttpTab) {\n' +
452
- ' httpView.style.display = "block";\n' +
453
- ' httpToolbar.style.display = "flex";\n' +
454
- ' httpCursor = 0;\n' +
455
- ' document.getElementById("http-body").innerHTML = "";\n' +
456
- ' fetchHttp();\n' +
457
- ' } else {\n' +
458
- ' logView.style.display = "block";\n' +
459
- ' activeSource = source;\n' +
460
- ' if (isZero) {\n' +
461
- ' zeroToolbar.style.display = "flex";\n' +
462
- ' activeLevel = "info";\n' +
463
- ' } else {\n' +
464
- ' toolbar.style.display = "flex";\n' +
465
- ' if (activeLevel === "info") { activeLevel = ""; document.getElementById("level-filter").value = ""; }\n' +
466
- ' }\n' +
467
- ' lastCursor = 0;\n' +
468
- ' logView.innerHTML = "";\n' +
469
- ' fetchLogs();\n' +
470
- ' }\n' +
471
- '}\n' +
472
- '\n' +
473
- '// standalone mode: hide header + tabs\n' +
474
- 'if (standalone) {\n' +
475
- ' document.getElementById("admin-header").style.display = "none";\n' +
476
- ' document.getElementById("tab-bar").style.display = "none";\n' +
477
- '}\n' +
478
- '// activate initial tab\n' +
479
- 'switchTab(initSource, false);\n' +
480
- '\n' +
481
- 'document.getElementById("tab-bar").addEventListener("click", function(e) {\n' +
482
- ' var tab = e.target.closest(".tab");\n' +
483
- ' if (!tab) return;\n' +
484
- ' document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });\n' +
485
- ' tab.classList.add("active");\n' +
486
- ' switchTab(tab.dataset.source, true);\n' +
487
- '});\n' +
488
- '\n' +
489
- 'document.getElementById("level-filter").addEventListener("change", function(e) {\n' +
490
- ' activeLevel = e.target.value;\n' +
491
- ' lastCursor = 0;\n' +
492
- ' logView.innerHTML = "";\n' +
493
- ' fetchLogs();\n' +
494
- '});\n' +
495
- '\n' +
496
- 'document.getElementById("zero-level-filter").addEventListener("change", function(e) {\n' +
497
- ' activeLevel = e.target.value;\n' +
498
- ' lastCursor = 0;\n' +
499
- ' logView.innerHTML = "";\n' +
500
- ' fetchLogs();\n' +
501
- '});\n' +
502
- '\n' +
503
- 'var httpFilterTimeout = null;\n' +
504
- 'document.getElementById("http-path-filter").addEventListener("input", function() {\n' +
505
- ' clearTimeout(httpFilterTimeout);\n' +
506
- ' httpFilterTimeout = setTimeout(function() {\n' +
507
- ' httpCursor = 0;\n' +
508
- ' document.getElementById("http-body").innerHTML = "";\n' +
509
- ' fetchHttp();\n' +
510
- ' }, 300);\n' +
511
- '});\n' +
512
- '\n' +
513
- 'logView.addEventListener("scroll", function() {\n' +
514
- ' var atBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 40;\n' +
515
- ' autoScroll = atBottom;\n' +
516
- ' jumpBtn.classList.toggle("visible", !atBottom);\n' +
517
- '});\n' +
518
- '\n' +
519
- 'httpView.addEventListener("scroll", function() {\n' +
520
- ' var atBottom = httpView.scrollHeight - httpView.scrollTop - httpView.clientHeight < 40;\n' +
521
- ' httpAutoScroll = atBottom;\n' +
522
- '});\n' +
523
- '\n' +
524
- 'function jumpToBottom() {\n' +
525
- ' var el = isHttpTab ? httpView : logView;\n' +
526
- ' el.scrollTop = el.scrollHeight;\n' +
527
- ' autoScroll = true;\n' +
528
- ' httpAutoScroll = true;\n' +
529
- ' jumpBtn.classList.remove("visible");\n' +
530
- '}\n' +
531
- '\n' +
532
- 'function fmtTime(ts) {\n' +
533
- ' var d = new Date(ts);\n' +
534
- ' return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })\n' +
535
- ' + "." + String(d.getMilliseconds()).padStart(3, "0");\n' +
536
- '}\n' +
537
- '\n' +
538
- 'function escHtml(s) {\n' +
539
- ' return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");\n' +
540
- '}\n' +
541
- '\n' +
542
- 'function fmtSize(bytes) {\n' +
543
- ' if (bytes === 0) return "-";\n' +
544
- ' if (bytes < 1024) return bytes + "B";\n' +
545
- ' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "kb";\n' +
546
- ' return (bytes / (1024 * 1024)).toFixed(1) + "MB";\n' +
547
- '}\n' +
548
- '\n' +
549
- 'function renderEntries(entries) {\n' +
550
- ' var frag = document.createDocumentFragment();\n' +
551
- ' for (var i = 0; i < entries.length; i++) {\n' +
552
- ' var e = entries[i];\n' +
553
- ' var div = document.createElement("div");\n' +
554
- ' div.className = "log-line level-" + e.level;\n' +
555
- ' div.innerHTML = \'<span class="ts">\' + fmtTime(e.ts) + "</span> "\n' +
556
- ' + \'<span class="src \' + e.source + \'">\' + e.source.padEnd(6) + "</span> "\n' +
557
- ' + \'<span class="msg">\' + escHtml(e.msg) + "</span>";\n' +
558
- ' frag.appendChild(div);\n' +
559
- ' }\n' +
560
- ' logView.appendChild(frag);\n' +
561
- ' if (autoScroll) logView.scrollTop = logView.scrollHeight;\n' +
562
- '}\n' +
563
- '\n' +
564
- 'function renderHttpEntries(entries) {\n' +
565
- ' var tbody = document.getElementById("http-body");\n' +
566
- ' var frag = document.createDocumentFragment();\n' +
567
- ' for (var i = 0; i < entries.length; i++) {\n' +
568
- ' var e = entries[i];\n' +
569
- ' var tr = document.createElement("tr");\n' +
570
- ' tr.className = "http-row";\n' +
571
- ' tr.dataset.id = e.id;\n' +
572
- ' var mc = e.method.toLowerCase();\n' +
573
- ' var sc = "s" + String(e.status).charAt(0);\n' +
574
- ' tr.innerHTML = "<td>" + fmtTime(e.ts) + "</td>"\n' +
575
- ' + \'<td><span class="method \' + mc + \'">\' + e.method + "</span></td>"\n' +
576
- ' + \'<td class="path">\' + escHtml(e.path) + "</td>"\n' +
577
- ' + \'<td><span class="status \' + sc + \'">\' + e.status + "</span></td>"\n' +
578
- ' + \'<td class="dur">\' + e.duration + "ms</td>"\n' +
579
- ' + \'<td class="sz">\' + fmtSize(e.resSize) + "</td>";\n' +
580
- ' tr.addEventListener("click", (function(entry) {\n' +
581
- ' return function() { toggleHttpDetail(this, entry); };\n' +
582
- ' })(e));\n' +
583
- ' frag.appendChild(tr);\n' +
584
- ' }\n' +
585
- ' tbody.appendChild(frag);\n' +
586
- ' if (httpAutoScroll) httpView.scrollTop = httpView.scrollHeight;\n' +
587
- '}\n' +
588
- '\n' +
589
- 'function toggleHttpDetail(row, entry) {\n' +
590
- ' var next = row.nextElementSibling;\n' +
591
- ' if (next && next.classList.contains("http-detail")) {\n' +
592
- ' next.classList.toggle("open");\n' +
593
- ' return;\n' +
594
- ' }\n' +
595
- ' var detail = document.createElement("tr");\n' +
596
- ' detail.className = "http-detail open";\n' +
597
- ' var html = \'<td colspan="6">\';\n' +
598
- ' html += \'<div class="hdr-section"><div class="hdr-title">request headers</div>\';\n' +
599
- ' var rk = Object.keys(entry.reqHeaders || {}).sort();\n' +
600
- ' for (var i = 0; i < rk.length; i++) {\n' +
601
- ' 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' +
602
- ' }\n' +
603
- ' html += "</div>";\n' +
604
- ' html += \'<div class="hdr-section"><div class="hdr-title">response headers</div>\';\n' +
605
- ' var sk = Object.keys(entry.resHeaders || {}).sort();\n' +
606
- ' for (var j = 0; j < sk.length; j++) {\n' +
607
- ' 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' +
608
- ' }\n' +
609
- ' html += "</div>";\n' +
610
- ' 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' +
611
- ' html += "</td>";\n' +
612
- ' detail.innerHTML = html;\n' +
613
- ' row.parentNode.insertBefore(detail, row.nextSibling);\n' +
614
- '}\n' +
615
- '\n' +
616
- 'function fetchLogs() {\n' +
617
- ' var params = new URLSearchParams();\n' +
618
- ' if (activeSource) params.set("source", activeSource);\n' +
619
- ' if (activeLevel) params.set("level", activeLevel);\n' +
620
- ' if (lastCursor) params.set("since", String(lastCursor));\n' +
621
- ' fetch("/api/logs?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
622
- ' if (data.entries && data.entries.length > 0) renderEntries(data.entries);\n' +
623
- ' if (data.cursor) lastCursor = data.cursor;\n' +
624
- ' }).catch(function() {});\n' +
625
- '}\n' +
626
- '\n' +
627
- 'function fetchHttp() {\n' +
628
- ' var params = new URLSearchParams();\n' +
629
- ' if (httpCursor) params.set("since", String(httpCursor));\n' +
630
- ' var pathFilter = document.getElementById("http-path-filter").value;\n' +
631
- ' if (pathFilter) params.set("path", pathFilter);\n' +
632
- ' fetch("/api/http-log?" + params).then(function(res) { return res.json(); }).then(function(data) {\n' +
633
- ' if (data.entries && data.entries.length > 0) renderHttpEntries(data.entries);\n' +
634
- ' if (data.cursor) httpCursor = data.cursor;\n' +
635
- ' }).catch(function() {});\n' +
636
- '}\n' +
637
- '\n' +
638
- 'function loadEnv() {\n' +
639
- ' fetch("/api/env").then(function(res) { return res.json(); }).then(function(data) {\n' +
640
- ' var tbody = document.getElementById("env-body");\n' +
641
- ' tbody.innerHTML = "";\n' +
642
- ' var keys = Object.keys(data.env).sort();\n' +
643
- ' for (var i = 0; i < keys.length; i++) {\n' +
644
- ' var tr = document.createElement("tr");\n' +
645
- ' tr.innerHTML = "<td>" + escHtml(keys[i]) + "</td><td>" + escHtml(String(data.env[keys[i]])) + "</td>";\n' +
646
- ' tbody.appendChild(tr);\n' +
647
- ' }\n' +
648
- ' envLoaded = true;\n' +
649
- ' }).catch(function() {});\n' +
650
- '}\n' +
651
- '\n' +
652
- 'function fetchStatus() {\n' +
653
- ' fetch("/api/status").then(function(res) { return res.json(); }).then(function(data) {\n' +
654
- ' document.getElementById("pg-port").textContent = ":" + data.pgPort;\n' +
655
- ' document.getElementById("zero-port").textContent = ":" + data.zeroPort;\n' +
656
- ' document.getElementById("sqlite-badge").textContent = "sqlite: " + (data.sqliteMode || "wasm");\n' +
657
- ' var m = Math.floor(data.uptime / 60);\n' +
658
- ' var s = data.uptime % 60;\n' +
659
- ' document.getElementById("uptime-badge").textContent = "\\u23F1 " + (m > 0 ? m + "m " : "") + s + "s";\n' +
660
- ' var zeroDisabled = data.skipZeroCache;\n' +
661
- ' document.querySelectorAll("[data-zero-action]").forEach(function(btn) {\n' +
662
- ' btn.disabled = zeroDisabled;\n' +
663
- ' });\n' +
664
- ' }).catch(function() {});\n' +
665
- '}\n' +
666
- '\n' +
667
- 'function doAction(action, btn) {\n' +
668
- ' if (action === "reset-zero") {\n' +
669
- ' if (!confirm("Reset zero-cache? This deletes the replica and resyncs from scratch.")) return;\n' +
670
- ' }\n' +
671
- ' if (action === "reset-zero-full") {\n' +
672
- ' if (!confirm("Full reset zero state? This deletes CVR, CDB, and replica databases. Use after schema changes.")) return;\n' +
673
- ' }\n' +
674
- ' btn.disabled = true;\n' +
675
- ' var origText = btn.textContent;\n' +
676
- ' btn.textContent = "...";\n' +
677
- ' fetch("/api/actions/" + action, { method: "POST" })\n' +
678
- ' .then(function(res) { return res.json(); })\n' +
679
- ' .then(function(data) {\n' +
680
- ' showToast(data.message || "done", data.ok ? "success" : "error");\n' +
681
- ' if (action === "clear-logs") {\n' +
682
- ' logView.innerHTML = "";\n' +
683
- ' lastCursor = 0;\n' +
684
- ' }\n' +
685
- ' if (action === "clear-http") {\n' +
686
- ' document.getElementById("http-body").innerHTML = "";\n' +
687
- ' httpCursor = 0;\n' +
688
- ' }\n' +
689
- ' })\n' +
690
- ' .catch(function(err) {\n' +
691
- ' showToast("failed: " + err.message, "error");\n' +
692
- ' })\n' +
693
- ' .finally(function() {\n' +
694
- ' btn.disabled = false;\n' +
695
- ' btn.textContent = origText;\n' +
696
- ' });\n' +
697
- '}\n' +
698
- '\n' +
699
- 'function showToast(msg, type) {\n' +
700
- ' toastEl.textContent = msg;\n' +
701
- ' toastEl.className = "toast " + type + " show";\n' +
702
- ' setTimeout(function() { toastEl.className = "toast"; }, 2500);\n' +
703
- '}\n' +
704
- '\n' +
705
- 'fetchStatus();\n' +
706
- 'setInterval(function() {\n' +
707
- ' if (document.hidden) return;\n' +
708
- ' if (isHttpTab) fetchHttp();\n' +
709
- ' else if (!isEnvTab) fetchLogs();\n' +
710
- '}, 1000);\n' +
711
- 'setInterval(function() { if (!document.hidden) fetchStatus(); }, 5000);\n' +
712
- 'document.addEventListener("visibilitychange", function() {\n' +
713
- ' if (document.hidden) return;\n' +
714
- ' if (isHttpTab) fetchHttp();\n' +
715
- ' else if (!isEnvTab) fetchLogs();\n' +
716
- ' fetchStatus();\n' +
717
- '});\n' +
718
- 'window.addEventListener("popstate", function() {\n' +
719
- ' var p = window.location.pathname.replace(/\\/$/, "") || "/";\n' +
720
- ' var s = pathMap[p] !== undefined ? pathMap[p] : "";\n' +
721
- ' var tab = document.querySelector(".tab[data-source=\\"" + s + "\\"]");\n' +
722
- ' if (tab) tab.click();\n' +
723
- '});\n' +
724
- '</script>\n' +
725
- '</body>\n' +
726
- '</html>');
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>oreZ admin</title>
8
+ <style>
9
+ :root {
10
+ --bg: #000;
11
+ --surface: #0a0a0a;
12
+ --border: #222;
13
+ --text: #fff;
14
+ --text-dim: #666;
15
+ --accent: #fff;
16
+ --green: #888;
17
+ --yellow: #999;
18
+ --red: #f55;
19
+ --purple: #aaa;
20
+ }
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
23
+ ::-webkit-scrollbar-track { background: var(--bg); }
24
+ ::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
25
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro", system-ui, sans-serif;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ height: 100vh;
31
+ display: flex;
32
+ flex-direction: column;
33
+ overflow: hidden;
34
+ }
35
+ .header {
36
+ display: flex;
37
+ align-items: center;
38
+ padding: 4px 12px;
39
+ background: var(--surface);
40
+ border-bottom: 0.5px solid var(--border);
41
+ gap: 8px;
42
+ flex-shrink: 0;
43
+ }
44
+ .header .logo {
45
+ font-size: 12px;
46
+ font-weight: 700;
47
+ color: var(--accent);
48
+ letter-spacing: -0.5px;
49
+ }
50
+ .badge {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ padding: 1px 6px;
54
+ border-radius: 12px;
55
+ font-size: 10px;
56
+ border: 0.5px solid var(--border);
57
+ color: var(--text-dim);
58
+ gap: 3px;
59
+ }
60
+ .badge .dot {
61
+ width: 6px;
62
+ height: 6px;
63
+ border-radius: 50%;
64
+ background: var(--green);
65
+ }
66
+ .spacer { flex: 1; }
67
+ .tabs {
68
+ display: flex;
69
+ padding: 0 12px;
70
+ background: var(--surface);
71
+ border-bottom: 0.5px solid var(--border);
72
+ gap: 0;
73
+ flex-shrink: 0;
74
+ }
75
+ .tab {
76
+ padding: 4px 10px;
77
+ font-size: 11px;
78
+ color: var(--text-dim);
79
+ cursor: pointer;
80
+ border-bottom: 2px solid transparent;
81
+ transition: all 0.15s;
82
+ background: none;
83
+ border-top: none;
84
+ border-left: none;
85
+ border-right: none;
86
+ font-family: inherit;
87
+ }
88
+ .tab:hover { color: var(--text); }
89
+ .tab.active {
90
+ color: var(--accent);
91
+ border-bottom-color: var(--accent);
92
+ }
93
+ .toolbar {
94
+ display: flex;
95
+ align-items: center;
96
+ padding: 3px 12px;
97
+ gap: 8px;
98
+ border-bottom: 0.5px solid var(--border);
99
+ flex-shrink: 0;
100
+ }
101
+ .toolbar label {
102
+ font-size: 10px;
103
+ color: var(--text-dim);
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.5px;
106
+ }
107
+ .toolbar select {
108
+ background: var(--surface);
109
+ color: var(--text);
110
+ border: 0.5px solid var(--border);
111
+ border-radius: 4px;
112
+ padding: 2px 6px;
113
+ font-size: 11px;
114
+ font-family: inherit;
115
+ cursor: pointer;
116
+ }
117
+ .toolbar select:focus { outline: none; border-color: var(--accent); }
118
+ .toolbar input[type="text"] {
119
+ background: var(--surface);
120
+ color: var(--text);
121
+ border: 0.5px solid var(--border);
122
+ border-radius: 4px;
123
+ padding: 2px 6px;
124
+ font-size: 11px;
125
+ font-family: inherit;
126
+ width: 180px;
127
+ }
128
+ .toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }
129
+ .toolbar input[type="text"]::placeholder { color: var(--text-dim); }
130
+ .sep { width: 1px; height: 20px; background: var(--border); }
131
+ .action-btn {
132
+ padding: 2px 8px;
133
+ border-radius: 4px;
134
+ border: 1px solid;
135
+ background: transparent;
136
+ cursor: pointer;
137
+ font-family: inherit;
138
+ font-size: 10px;
139
+ transition: all 0.15s ease;
140
+ white-space: nowrap;
141
+ }
142
+ .action-btn:disabled { opacity: 0.4; cursor: not-allowed; }
143
+ .action-btn.blue { color: var(--accent); border-color: #ffffff22; }
144
+ .action-btn.blue:hover:not(:disabled) { background: #ffffff11; border-color: var(--accent); }
145
+ .action-btn.orange { color: var(--yellow); border-color: #ffffff22; }
146
+ .action-btn.orange:hover:not(:disabled) { background: #ffffff11; border-color: var(--yellow); }
147
+ .action-btn.red { color: var(--red); border-color: #ff555522; }
148
+ .action-btn.red:hover:not(:disabled) { background: #ff555511; border-color: var(--red); }
149
+ .action-btn.gray { color: var(--text-dim); border-color: #ffffff22; }
150
+ .action-btn.gray:hover:not(:disabled) { background: #ffffff11; border-color: var(--text-dim); }
151
+ .content-area {
152
+ flex: 1;
153
+ overflow: hidden;
154
+ position: relative;
155
+ display: flex;
156
+ flex-direction: column;
157
+ }
158
+ .log-wrap {
159
+ flex: 1;
160
+ overflow: hidden;
161
+ position: relative;
162
+ }
163
+ .log-view {
164
+ height: 100%;
165
+ overflow-y: auto;
166
+ padding: 4px 12px;
167
+ font-size: 11px;
168
+ line-height: 1.4;
169
+ }
170
+ .log-line { white-space: pre-wrap; word-break: break-all; }
171
+ .log-line .ts { color: var(--text-dim); }
172
+ .log-line .src { display: inline-block; width: 7ch; }
173
+ .log-line .src.zero { color: var(--purple); }
174
+ .log-line .src.pglite { color: var(--green); }
175
+ .log-line .src.proxy { color: var(--yellow); }
176
+ .log-line .src.orez { color: var(--accent); }
177
+ .log-line .src.s3 { color: #888; }
178
+ .log-line.level-error .msg { color: var(--red); }
179
+ .log-line.level-warn .msg { color: var(--yellow); }
180
+ .log-line.level-info .msg { color: var(--text); }
181
+ .log-line.level-debug .msg { color: var(--text-dim); }
182
+ .jump-btn {
183
+ position: absolute;
184
+ bottom: 16px;
185
+ left: 50%;
186
+ transform: translateX(-50%);
187
+ padding: 6px 16px;
188
+ border-radius: 20px;
189
+ background: #333;
190
+ color: var(--text);
191
+ border: 1px solid var(--border);
192
+ font-size: 12px;
193
+ font-family: inherit;
194
+ cursor: pointer;
195
+ opacity: 0;
196
+ transition: opacity 0.2s;
197
+ pointer-events: none;
198
+ z-index: 10;
199
+ }
200
+ .jump-btn.visible { opacity: 1; pointer-events: auto; }
201
+ .env-view {
202
+ height: 100%;
203
+ overflow-y: auto;
204
+ padding: 16px;
205
+ display: none;
206
+ }
207
+ .env-table { width: 100%; border-collapse: collapse; font-size: 12px; }
208
+ .env-table th {
209
+ text-align: left;
210
+ padding: 6px 12px;
211
+ color: var(--text-dim);
212
+ border-bottom: 0.5px solid var(--border);
213
+ font-weight: 500;
214
+ text-transform: uppercase;
215
+ font-size: 10px;
216
+ letter-spacing: 0.5px;
217
+ }
218
+ .env-table td {
219
+ padding: 6px 12px;
220
+ border-bottom: 0.5px solid var(--border);
221
+ }
222
+ .env-table td:first-child { color: var(--accent); white-space: nowrap; }
223
+ .env-table td:last-child { color: var(--text); word-break: break-all; }
224
+ .env-table tr:hover td { background: #111; }
225
+ .http-view {
226
+ height: 100%;
227
+ overflow-y: auto;
228
+ padding: 0;
229
+ display: none;
230
+ }
231
+ .http-table { width: 100%; border-collapse: collapse; font-size: 12px; }
232
+ .http-table th {
233
+ text-align: left;
234
+ padding: 6px 12px;
235
+ color: var(--text-dim);
236
+ border-bottom: 0.5px solid var(--border);
237
+ font-weight: 500;
238
+ text-transform: uppercase;
239
+ font-size: 10px;
240
+ letter-spacing: 0.5px;
241
+ position: sticky;
242
+ top: 0;
243
+ background: var(--bg);
244
+ z-index: 1;
245
+ }
246
+ .http-table td {
247
+ padding: 5px 12px;
248
+ border-bottom: 0.5px solid var(--border);
249
+ white-space: nowrap;
250
+ }
251
+ .http-table tr.http-row { cursor: pointer; }
252
+ .http-table tr.http-row:hover td { background: #111; }
253
+ .http-table .method { font-weight: 600; }
254
+ .http-table .method.get { color: var(--green); }
255
+ .http-table .method.post { color: var(--yellow); }
256
+ .http-table .method.put { color: var(--accent); }
257
+ .http-table .method.delete { color: var(--red); }
258
+ .http-table .method.patch { color: #888; }
259
+ .http-table .method.ws { color: var(--purple); }
260
+ .http-table .status.s2 { color: var(--green); }
261
+ .http-table .status.s3 { color: var(--yellow); }
262
+ .http-table .status.s4 { color: var(--red); }
263
+ .http-table .status.s5 { color: var(--red); font-weight: 600; }
264
+ .http-table .path { color: var(--text); max-width: 500px; overflow: hidden; text-overflow: ellipsis; }
265
+ .http-table .dur { color: var(--text-dim); }
266
+ .http-table .sz { color: var(--text-dim); }
267
+ .http-detail {
268
+ display: none;
269
+ }
270
+ .http-detail.open { display: table-row; }
271
+ .http-detail td {
272
+ padding: 8px 12px 12px 24px;
273
+ background: #080808;
274
+ border-bottom: 0.5px solid var(--border);
275
+ }
276
+ .http-detail .hdr-section { margin-bottom: 8px; }
277
+ .http-detail .hdr-title {
278
+ font-size: 10px;
279
+ text-transform: uppercase;
280
+ color: var(--text-dim);
281
+ letter-spacing: 0.5px;
282
+ margin-bottom: 4px;
283
+ }
284
+ .http-detail .hdr-line {
285
+ font-size: 11px;
286
+ line-height: 1.6;
287
+ white-space: pre-wrap;
288
+ word-break: break-all;
289
+ }
290
+ .http-detail .hdr-key { color: var(--accent); }
291
+ .http-detail .hdr-val { color: var(--text-dim); }
292
+ .toolbar-actions {
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 6px;
296
+ margin-left: auto;
297
+ }
298
+ /* data explorer */
299
+ .data-view {
300
+ height: 100%;
301
+ display: none;
302
+ flex-direction: column;
303
+ overflow: hidden;
304
+ }
305
+ .data-view.visible {
306
+ display: flex;
307
+ }
308
+ .data-toolbar {
309
+ display: flex;
310
+ align-items: center;
311
+ padding: 3px 12px;
312
+ gap: 8px;
313
+ border-bottom: 0.5px solid var(--border);
314
+ flex-shrink: 0;
315
+ }
316
+ .data-sub-tabs {
317
+ display: flex;
318
+ gap: 0;
319
+ }
320
+ .data-sub-tab {
321
+ padding: 3px 10px;
322
+ font-size: 10px;
323
+ color: var(--text-dim);
324
+ cursor: pointer;
325
+ background: none;
326
+ border: none;
327
+ border-bottom: 2px solid transparent;
328
+ font-family: inherit;
329
+ text-transform: uppercase;
330
+ letter-spacing: 0.5px;
331
+ transition: all 0.15s;
332
+ }
333
+ .data-sub-tab:hover { color: var(--text); }
334
+ .data-sub-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
335
+ .data-content {
336
+ flex: 1;
337
+ display: flex;
338
+ overflow: hidden;
339
+ }
340
+ .data-sidebar {
341
+ width: 220px;
342
+ border-right: 0.5px solid var(--border);
343
+ overflow-y: auto;
344
+ flex-shrink: 0;
345
+ }
346
+ .data-sidebar-header {
347
+ padding: 6px 10px;
348
+ font-size: 10px;
349
+ color: var(--text-dim);
350
+ text-transform: uppercase;
351
+ letter-spacing: 0.5px;
352
+ border-bottom: 0.5px solid var(--border);
353
+ position: sticky;
354
+ top: 0;
355
+ background: var(--bg);
356
+ }
357
+ .data-table-item {
358
+ padding: 4px 10px;
359
+ font-size: 11px;
360
+ cursor: pointer;
361
+ display: flex;
362
+ justify-content: space-between;
363
+ align-items: center;
364
+ border-bottom: 0.5px solid transparent;
365
+ transition: background 0.1s;
366
+ }
367
+ .data-table-item:hover { background: #111; }
368
+ .data-table-item.active { background: #181818; color: var(--accent); }
369
+ .data-table-item .tbl-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
370
+ .data-table-item .tbl-size { color: var(--text-dim); font-size: 10px; flex-shrink: 0; margin-left: 8px; }
371
+ .data-main {
372
+ flex: 1;
373
+ display: flex;
374
+ flex-direction: column;
375
+ overflow: hidden;
376
+ }
377
+ .sql-editor-wrap {
378
+ border-bottom: 0.5px solid var(--border);
379
+ display: flex;
380
+ flex-direction: column;
381
+ flex-shrink: 0;
382
+ }
383
+ .sql-editor {
384
+ width: 100%;
385
+ background: var(--surface);
386
+ color: var(--text);
387
+ border: none;
388
+ padding: 8px 12px;
389
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
390
+ font-size: 12px;
391
+ line-height: 1.5;
392
+ resize: vertical;
393
+ min-height: 60px;
394
+ max-height: 300px;
395
+ outline: none;
396
+ }
397
+ .sql-editor::placeholder { color: #444; }
398
+ .sql-bar {
399
+ display: flex;
400
+ align-items: center;
401
+ padding: 3px 8px;
402
+ gap: 8px;
403
+ background: var(--surface);
404
+ border-top: 0.5px solid var(--border);
405
+ }
406
+ .sql-bar .sql-status {
407
+ font-size: 10px;
408
+ color: var(--text-dim);
409
+ flex: 1;
410
+ }
411
+ .sql-bar .sql-status.error { color: var(--red); }
412
+ .data-results {
413
+ flex: 1;
414
+ overflow: auto;
415
+ }
416
+ .data-results table {
417
+ width: 100%;
418
+ border-collapse: collapse;
419
+ font-size: 11px;
420
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
421
+ }
422
+ .data-results th {
423
+ text-align: left;
424
+ padding: 4px 10px;
425
+ color: var(--text-dim);
426
+ border-bottom: 0.5px solid var(--border);
427
+ font-weight: 500;
428
+ font-size: 10px;
429
+ letter-spacing: 0.3px;
430
+ position: sticky;
431
+ top: 0;
432
+ background: var(--bg);
433
+ z-index: 1;
434
+ cursor: pointer;
435
+ user-select: none;
436
+ white-space: nowrap;
437
+ }
438
+ .data-results th:hover { color: var(--text); }
439
+ .data-results td {
440
+ padding: 3px 10px;
441
+ border-bottom: 0.5px solid #1a1a1a;
442
+ white-space: nowrap;
443
+ max-width: 300px;
444
+ overflow: hidden;
445
+ text-overflow: ellipsis;
446
+ }
447
+ .data-results tr:hover td { background: #0a0a0a; }
448
+ .data-results td.null-val { color: #444; font-style: italic; }
449
+ .data-results tr.clickable { cursor: pointer; }
450
+ .data-results tr.clickable:hover td { background: #111; }
451
+ .data-empty {
452
+ display: flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ height: 100%;
456
+ color: var(--text-dim);
457
+ font-size: 12px;
458
+ }
459
+ .data-paging {
460
+ display: flex;
461
+ align-items: center;
462
+ justify-content: space-between;
463
+ padding: 4px 10px;
464
+ border-top: 0.5px solid var(--border);
465
+ font-size: 10px;
466
+ color: var(--text-dim);
467
+ flex-shrink: 0;
468
+ }
469
+ .data-paging button {
470
+ background: none;
471
+ border: 0.5px solid var(--border);
472
+ color: var(--text-dim);
473
+ padding: 2px 8px;
474
+ border-radius: 3px;
475
+ font-size: 10px;
476
+ cursor: pointer;
477
+ font-family: inherit;
478
+ }
479
+ .data-paging button:hover { color: var(--text); border-color: var(--text-dim); }
480
+ .data-paging button:disabled { opacity: 0.3; cursor: not-allowed; }
481
+ .data-search {
482
+ padding: 4px 10px;
483
+ border-bottom: 0.5px solid var(--border);
484
+ }
485
+ .data-search input {
486
+ width: 100%;
487
+ background: var(--bg);
488
+ color: var(--text);
489
+ border: 0.5px solid var(--border);
490
+ border-radius: 3px;
491
+ padding: 3px 8px;
492
+ font-size: 11px;
493
+ font-family: inherit;
494
+ outline: none;
495
+ }
496
+ .data-search input:focus { border-color: var(--accent); }
497
+ .data-search input::placeholder { color: #444; }
498
+ /* row detail overlay */
499
+ .row-detail-overlay {
500
+ position: fixed;
501
+ top: 0; left: 0; right: 0; bottom: 0;
502
+ background: rgba(0,0,0,0.6);
503
+ z-index: 50;
504
+ display: none;
505
+ align-items: center;
506
+ justify-content: center;
507
+ }
508
+ .row-detail-overlay.open { display: flex; }
509
+ .row-detail-panel {
510
+ background: var(--bg);
511
+ border: 0.5px solid var(--border);
512
+ border-radius: 8px;
513
+ max-width: 700px;
514
+ width: 90%;
515
+ max-height: 80vh;
516
+ overflow-y: auto;
517
+ padding: 0;
518
+ }
519
+ .row-detail-header {
520
+ display: flex;
521
+ align-items: center;
522
+ padding: 8px 14px;
523
+ border-bottom: 0.5px solid var(--border);
524
+ font-size: 11px;
525
+ color: var(--text-dim);
526
+ position: sticky;
527
+ top: 0;
528
+ background: var(--bg);
529
+ }
530
+ .row-detail-header .spacer { flex: 1; }
531
+ .row-detail-close {
532
+ background: none;
533
+ border: none;
534
+ color: var(--text-dim);
535
+ font-size: 16px;
536
+ cursor: pointer;
537
+ padding: 0 4px;
538
+ }
539
+ .row-detail-close:hover { color: var(--text); }
540
+ .row-detail-body {
541
+ padding: 0;
542
+ }
543
+ .row-detail-field {
544
+ display: flex;
545
+ border-bottom: 0.5px solid #1a1a1a;
546
+ font-size: 12px;
547
+ }
548
+ .row-detail-field:last-child { border-bottom: none; }
549
+ .row-detail-key {
550
+ width: 180px;
551
+ flex-shrink: 0;
552
+ padding: 6px 14px;
553
+ color: var(--text-dim);
554
+ font-weight: 500;
555
+ border-right: 0.5px solid #1a1a1a;
556
+ word-break: break-all;
557
+ }
558
+ .row-detail-val {
559
+ flex: 1;
560
+ padding: 6px 14px;
561
+ color: var(--text);
562
+ white-space: pre-wrap;
563
+ word-break: break-all;
564
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
565
+ font-size: 11px;
566
+ }
567
+ .row-detail-val.null-val { color: #444; font-style: italic; }
568
+ /* toast */
569
+ .toast {
570
+ position: fixed;
571
+ bottom: 20px;
572
+ right: 20px;
573
+ padding: 10px 16px;
574
+ border-radius: 8px;
575
+ background: var(--surface);
576
+ border: 0.5px solid var(--border);
577
+ color: var(--text);
578
+ font-size: 12px;
579
+ font-family: inherit;
580
+ opacity: 0;
581
+ transform: translateY(10px);
582
+ transition: all 0.3s ease;
583
+ pointer-events: none;
584
+ z-index: 100;
585
+ }
586
+ .toast.show { opacity: 1; transform: translateY(0); }
587
+ .toast.error { border-color: var(--red); color: var(--red); }
588
+ .toast.success { border-color: var(--green); color: var(--green); }
589
+ </style>
590
+ </head>
591
+ <body>
592
+ <div class="header" id="admin-header">
593
+ <span class="logo">&#9670; oreZ admin</span>
594
+ <div class="spacer"></div>
595
+ <span class="badge"><span class="dot"></span> pg <span id="pg-port">-</span></span>
596
+ <span class="badge"><span class="dot"></span> zero <span id="zero-port">-</span></span>
597
+ <span class="badge" id="sqlite-badge">sqlite: --</span>
598
+ <span class="badge" id="uptime-badge">&#9201; --</span>
599
+ </div>
600
+
601
+ <div class="tabs" id="tab-bar">
602
+ <button class="tab active" data-source="data">Data</button>
603
+ <button class="tab" data-source="">Logs</button>
604
+ <button class="tab" data-source="zero">Zero</button>
605
+ <button class="tab" data-source="pglite">PGlite</button>
606
+ <button class="tab" data-source="proxy">Proxy</button>
607
+ <button class="tab" data-source="orez">Orez</button>
608
+ <button class="tab" data-source="s3">S3</button>
609
+ <button class="tab" data-source="http">HTTP</button>
610
+ <button class="tab" data-source="env">Env</button>
611
+ </div>
612
+
613
+ <div class="toolbar" id="toolbar" style="display:none">
614
+ <label>Level</label>
615
+ <select id="level-filter">
616
+ <option value="">all levels</option>
617
+ <option value="error">error only</option>
618
+ <option value="warn">warn+</option>
619
+ <option value="info">info+</option>
620
+ <option value="debug">debug+</option>
621
+ </select>
622
+ <div class="toolbar-actions" id="toolbar-log-actions">
623
+ <button class="action-btn gray" onclick="doAction('clear-logs', this)">&#x2715; Clear</button>
624
+ </div>
625
+ </div>
626
+
627
+ <div class="toolbar" id="zero-toolbar" style="display:none">
628
+ <label>Level</label>
629
+ <select id="zero-level-filter">
630
+ <option value="">all levels</option>
631
+ <option value="error">error only</option>
632
+ <option value="warn">warn+</option>
633
+ <option value="info" selected>info+</option>
634
+ <option value="debug">debug+</option>
635
+ </select>
636
+ <div class="toolbar-actions">
637
+ <button class="action-btn blue" data-zero-action onclick="doAction('restart-zero', this)">&#x21bb; Restart</button>
638
+ <button class="action-btn orange" data-zero-action onclick="doAction('reset-zero', this)">&#x21ba; Reset</button>
639
+ <button class="action-btn red" data-zero-action onclick="doAction('reset-zero-full', this)">&#x26a0; Full</button>
640
+ <button class="action-btn gray" onclick="doAction('clear-logs', this)">&#x2715; Clear</button>
641
+ </div>
642
+ </div>
643
+
644
+ <div class="toolbar" id="http-toolbar" style="display:none">
645
+ <label>Filter</label>
646
+ <input type="text" id="http-path-filter" placeholder="filter by path...">
647
+ <div class="toolbar-actions">
648
+ <button class="action-btn gray" onclick="doAction('clear-http', this)">&#x2715; Clear</button>
649
+ </div>
650
+ </div>
651
+
652
+ <div class="content-area">
653
+ <div class="data-view" id="data-view">
654
+ <div class="data-toolbar">
655
+ <div class="data-sub-tabs" id="data-sub-tabs">
656
+ <button class="data-sub-tab active" data-db="postgres">Main</button>
657
+ <button class="data-sub-tab" data-db="cvr">CVR</button>
658
+ <button class="data-sub-tab" data-db="cdb">CDB</button>
659
+ <button class="data-sub-tab" data-db="sqlite">SQLite</button>
660
+ </div>
661
+ </div>
662
+ <div class="data-content">
663
+ <div class="data-sidebar" id="data-sidebar">
664
+ <div class="data-sidebar-header">Tables</div>
665
+ <div class="data-search"><input type="text" id="data-table-search" placeholder="filter tables..."></div>
666
+ <div id="data-table-list"></div>
667
+ </div>
668
+ <div class="data-main">
669
+ <div class="sql-editor-wrap">
670
+ <textarea class="sql-editor" id="sql-editor" rows="3" placeholder="SELECT * FROM ... (Cmd+Enter to run)" spellcheck="false"></textarea>
671
+ <div class="sql-bar">
672
+ <span class="sql-status" id="sql-status"></span>
673
+ <button class="action-btn blue" id="sql-run-btn" onclick="runSql()">&#9654; Run</button>
674
+ </div>
675
+ </div>
676
+ <div class="data-results" id="data-results">
677
+ <div class="data-empty">select a table or run a query</div>
678
+ </div>
679
+ <div class="data-paging" id="data-paging" style="display:none">
680
+ <span id="data-paging-info"></span>
681
+ <div>
682
+ <button id="data-prev-btn" onclick="browseTable(-1)" disabled>&#x25C0; Prev</button>
683
+ <button id="data-next-btn" onclick="browseTable(1)">Next &#x25B6;</button>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+ </div>
689
+
690
+ <div class="row-detail-overlay" id="row-detail-overlay">
691
+ <div class="row-detail-panel">
692
+ <div class="row-detail-header">
693
+ <span>Row detail</span>
694
+ <div class="spacer"></div>
695
+ <button class="row-detail-close" onclick="closeRowDetail()">&#x2715;</button>
696
+ </div>
697
+ <div class="row-detail-body" id="row-detail-body"></div>
698
+ </div>
699
+ </div>
700
+
701
+ <div class="log-wrap">
702
+ <div class="log-view" id="log-view"></div>
703
+ <div class="env-view" id="env-view">
704
+ <table class="env-table">
705
+ <thead><tr><th>Variable</th><th>Value</th></tr></thead>
706
+ <tbody id="env-body"></tbody>
707
+ </table>
708
+ </div>
709
+ <div class="http-view" id="http-view">
710
+ <table class="http-table">
711
+ <thead><tr>
712
+ <th>Time</th>
713
+ <th>Method</th>
714
+ <th>Path</th>
715
+ <th>Status</th>
716
+ <th>Duration</th>
717
+ <th>Size</th>
718
+ </tr></thead>
719
+ <tbody id="http-body"></tbody>
720
+ </table>
721
+ </div>
722
+ <button class="jump-btn" id="jump-btn" onclick="jumpToBottom()">&#x2193; Jump to bottom</button>
723
+ </div>
724
+ </div>
725
+
726
+ <div class="toast" id="toast"></div>
727
+
728
+ <script>
729
+ // resolve initial tab from url path
730
+ var pathMap = {"/":"data","/data":"data","/all":"","/zero":"zero","/pglite":"pglite","/proxy":"proxy","/orez":"orez","/s3":"s3","/http":"http","/env":"env"};
731
+ var initPath = window.location.pathname.replace(/\\/$/, "") || "/";
732
+ var initSource = pathMap[initPath] !== undefined ? pathMap[initPath] : "data";
733
+ var standalone = initPath !== "/" && initPath !== "/data" && initPath !== "/all";
734
+ var activeSource = "";
735
+ var activeLevel = "";
736
+ var lastCursor = 0;
737
+ var autoScroll = true;
738
+ var envLoaded = false;
739
+ var isEnvTab = false;
740
+ var isHttpTab = false;
741
+ var isDataTab = initSource === "data";
742
+ var httpCursor = 0;
743
+ var httpAutoScroll = true;
744
+
745
+ // data explorer state
746
+ var dataDb = "postgres";
747
+ var dataTables = [];
748
+ var dataActiveTable = null;
749
+
750
+ var logView = document.getElementById("log-view");
751
+ var envView = document.getElementById("env-view");
752
+ var httpView = document.getElementById("http-view");
753
+ var dataView = document.getElementById("data-view");
754
+ var jumpBtn = document.getElementById("jump-btn");
755
+ var toastEl = document.getElementById("toast");
756
+ var toolbar = document.getElementById("toolbar");
757
+ var zeroToolbar = document.getElementById("zero-toolbar");
758
+ var httpToolbar = document.getElementById("http-toolbar");
759
+ var sqlEditor = document.getElementById("sql-editor");
760
+ var sqlStatus = document.getElementById("sql-status");
761
+ var dataResults = document.getElementById("data-results");
762
+
763
+ function sourceToPath(s) {
764
+ if (s === "data") return "/data";
765
+ return s ? "/" + s : "/all";
766
+ }
767
+
768
+ function switchTab(source, pushState) {
769
+ isEnvTab = source === "env";
770
+ isHttpTab = source === "http";
771
+ isDataTab = source === "data";
772
+ var isZero = source === "zero";
773
+ if (pushState) history.pushState(null, "", sourceToPath(source));
774
+ logView.style.display = "none";
775
+ envView.style.display = "none";
776
+ httpView.style.display = "none";
777
+ dataView.style.display = "none";
778
+ dataView.classList.remove("visible");
779
+ toolbar.style.display = "none";
780
+ zeroToolbar.style.display = "none";
781
+ httpToolbar.style.display = "none";
782
+ logView.parentElement.style.display = "none";
783
+ if (isDataTab) {
784
+ dataView.style.display = "flex";
785
+ dataView.classList.add("visible");
786
+ loadTables();
787
+ } else if (isEnvTab) {
788
+ logView.parentElement.style.display = "block";
789
+ envView.style.display = "block";
790
+ if (!envLoaded) loadEnv();
791
+ } else if (isHttpTab) {
792
+ logView.parentElement.style.display = "block";
793
+ httpView.style.display = "block";
794
+ httpToolbar.style.display = "flex";
795
+ httpCursor = 0;
796
+ document.getElementById("http-body").innerHTML = "";
797
+ fetchHttp();
798
+ } else {
799
+ logView.parentElement.style.display = "block";
800
+ logView.style.display = "block";
801
+ activeSource = source;
802
+ if (isZero) {
803
+ zeroToolbar.style.display = "flex";
804
+ activeLevel = "info";
805
+ } else {
806
+ toolbar.style.display = "flex";
807
+ if (activeLevel === "info") { activeLevel = ""; document.getElementById("level-filter").value = ""; }
808
+ }
809
+ lastCursor = 0;
810
+ logView.innerHTML = "";
811
+ fetchLogs();
812
+ }
813
+ }
814
+
815
+ // standalone mode: hide header + tabs
816
+ if (standalone) {
817
+ document.getElementById("admin-header").style.display = "none";
818
+ document.getElementById("tab-bar").style.display = "none";
819
+ }
820
+ // activate initial tab
821
+ switchTab(initSource, false);
822
+
823
+ document.getElementById("tab-bar").addEventListener("click", function(e) {
824
+ var tab = e.target.closest(".tab");
825
+ if (!tab) return;
826
+ document.querySelectorAll("#tab-bar .tab").forEach(function(t) { t.classList.remove("active"); });
827
+ tab.classList.add("active");
828
+ switchTab(tab.dataset.source, true);
829
+ });
830
+
831
+ document.getElementById("level-filter").addEventListener("change", function(e) {
832
+ activeLevel = e.target.value;
833
+ lastCursor = 0;
834
+ logView.innerHTML = "";
835
+ fetchLogs();
836
+ });
837
+
838
+ document.getElementById("zero-level-filter").addEventListener("change", function(e) {
839
+ activeLevel = e.target.value;
840
+ lastCursor = 0;
841
+ logView.innerHTML = "";
842
+ fetchLogs();
843
+ });
844
+
845
+ var httpFilterTimeout = null;
846
+ document.getElementById("http-path-filter").addEventListener("input", function() {
847
+ clearTimeout(httpFilterTimeout);
848
+ httpFilterTimeout = setTimeout(function() {
849
+ httpCursor = 0;
850
+ document.getElementById("http-body").innerHTML = "";
851
+ fetchHttp();
852
+ }, 300);
853
+ });
854
+
855
+ logView.addEventListener("scroll", function() {
856
+ var atBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 40;
857
+ autoScroll = atBottom;
858
+ jumpBtn.classList.toggle("visible", !atBottom);
859
+ });
860
+
861
+ httpView.addEventListener("scroll", function() {
862
+ var atBottom = httpView.scrollHeight - httpView.scrollTop - httpView.clientHeight < 40;
863
+ httpAutoScroll = atBottom;
864
+ });
865
+
866
+ function jumpToBottom() {
867
+ var el = isHttpTab ? httpView : logView;
868
+ el.scrollTop = el.scrollHeight;
869
+ autoScroll = true;
870
+ httpAutoScroll = true;
871
+ jumpBtn.classList.remove("visible");
872
+ }
873
+
874
+ function fmtTime(ts) {
875
+ var d = new Date(ts);
876
+ return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })
877
+ + "." + String(d.getMilliseconds()).padStart(3, "0");
878
+ }
879
+
880
+ function escHtml(s) {
881
+ return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
882
+ }
883
+
884
+ function fmtSize(bytes) {
885
+ if (bytes === 0 || bytes == null) return "-";
886
+ if (bytes < 1024) return bytes + "B";
887
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "kb";
888
+ return (bytes / (1024 * 1024)).toFixed(1) + "MB";
889
+ }
890
+
891
+ function renderEntries(entries) {
892
+ var frag = document.createDocumentFragment();
893
+ for (var i = 0; i < entries.length; i++) {
894
+ var e = entries[i];
895
+ var div = document.createElement("div");
896
+ div.className = "log-line level-" + e.level;
897
+ div.innerHTML = '<span class="ts">' + fmtTime(e.ts) + "</span> "
898
+ + '<span class="src ' + e.source + '">' + e.source.padEnd(6) + "</span> "
899
+ + '<span class="msg">' + escHtml(e.msg) + "</span>";
900
+ frag.appendChild(div);
901
+ }
902
+ logView.appendChild(frag);
903
+ if (autoScroll) logView.scrollTop = logView.scrollHeight;
904
+ }
905
+
906
+ function renderHttpEntries(entries) {
907
+ var tbody = document.getElementById("http-body");
908
+ var frag = document.createDocumentFragment();
909
+ for (var i = 0; i < entries.length; i++) {
910
+ var e = entries[i];
911
+ var tr = document.createElement("tr");
912
+ tr.className = "http-row";
913
+ tr.dataset.id = e.id;
914
+ var mc = e.method.toLowerCase();
915
+ var sc = "s" + String(e.status).charAt(0);
916
+ tr.innerHTML = "<td>" + fmtTime(e.ts) + "</td>"
917
+ + '<td><span class="method ' + mc + '">' + e.method + "</span></td>"
918
+ + '<td class="path">' + escHtml(e.path) + "</td>"
919
+ + '<td><span class="status ' + sc + '">' + e.status + "</span></td>"
920
+ + '<td class="dur">' + e.duration + "ms</td>"
921
+ + '<td class="sz">' + fmtSize(e.resSize) + "</td>";
922
+ tr.addEventListener("click", (function(entry) {
923
+ return function() { toggleHttpDetail(this, entry); };
924
+ })(e));
925
+ frag.appendChild(tr);
926
+ }
927
+ tbody.appendChild(frag);
928
+ if (httpAutoScroll) httpView.scrollTop = httpView.scrollHeight;
929
+ }
930
+
931
+ function toggleHttpDetail(row, entry) {
932
+ var next = row.nextElementSibling;
933
+ if (next && next.classList.contains("http-detail")) {
934
+ next.classList.toggle("open");
935
+ return;
936
+ }
937
+ var detail = document.createElement("tr");
938
+ detail.className = "http-detail open";
939
+ var html = '<td colspan="6">';
940
+ html += '<div class="hdr-section"><div class="hdr-title">request headers</div>';
941
+ var rk = Object.keys(entry.reqHeaders || {}).sort();
942
+ for (var i = 0; i < rk.length; i++) {
943
+ html += '<div class="hdr-line"><span class="hdr-key">' + escHtml(rk[i]) + '</span>: <span class="hdr-val">' + escHtml(entry.reqHeaders[rk[i]]) + "</span></div>";
944
+ }
945
+ html += "</div>";
946
+ html += '<div class="hdr-section"><div class="hdr-title">response headers</div>';
947
+ var sk = Object.keys(entry.resHeaders || {}).sort();
948
+ for (var j = 0; j < sk.length; j++) {
949
+ html += '<div class="hdr-line"><span class="hdr-key">' + escHtml(sk[j]) + '</span>: <span class="hdr-val">' + escHtml(entry.resHeaders[sk[j]]) + "</span></div>";
950
+ }
951
+ html += "</div>";
952
+ 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>";
953
+ html += "</td>";
954
+ detail.innerHTML = html;
955
+ row.parentNode.insertBefore(detail, row.nextSibling);
956
+ }
957
+
958
+ function fetchLogs() {
959
+ var params = new URLSearchParams();
960
+ if (activeSource) params.set("source", activeSource);
961
+ if (activeLevel) params.set("level", activeLevel);
962
+ if (lastCursor) params.set("since", String(lastCursor));
963
+ fetch("/api/logs?" + params).then(function(res) { return res.json(); }).then(function(data) {
964
+ if (data.entries && data.entries.length > 0) renderEntries(data.entries);
965
+ if (data.cursor) lastCursor = data.cursor;
966
+ }).catch(function() {});
967
+ }
968
+
969
+ function fetchHttp() {
970
+ var params = new URLSearchParams();
971
+ if (httpCursor) params.set("since", String(httpCursor));
972
+ var pathFilter = document.getElementById("http-path-filter").value;
973
+ if (pathFilter) params.set("path", pathFilter);
974
+ fetch("/api/http-log?" + params).then(function(res) { return res.json(); }).then(function(data) {
975
+ if (data.entries && data.entries.length > 0) renderHttpEntries(data.entries);
976
+ if (data.cursor) httpCursor = data.cursor;
977
+ }).catch(function() {});
978
+ }
979
+
980
+ function loadEnv() {
981
+ fetch("/api/env").then(function(res) { return res.json(); }).then(function(data) {
982
+ var tbody = document.getElementById("env-body");
983
+ tbody.innerHTML = "";
984
+ var keys = Object.keys(data.env).sort();
985
+ for (var i = 0; i < keys.length; i++) {
986
+ var tr = document.createElement("tr");
987
+ tr.innerHTML = "<td>" + escHtml(keys[i]) + "</td><td>" + escHtml(String(data.env[keys[i]])) + "</td>";
988
+ tbody.appendChild(tr);
989
+ }
990
+ envLoaded = true;
991
+ }).catch(function() {});
992
+ }
993
+
994
+ function fetchStatus() {
995
+ fetch("/api/status").then(function(res) { return res.json(); }).then(function(data) {
996
+ document.getElementById("pg-port").textContent = ":" + data.pgPort;
997
+ document.getElementById("zero-port").textContent = ":" + data.zeroPort;
998
+ document.getElementById("sqlite-badge").textContent = "sqlite: " + (data.sqliteMode || "wasm");
999
+ var m = Math.floor(data.uptime / 60);
1000
+ var s = data.uptime % 60;
1001
+ document.getElementById("uptime-badge").textContent = "\\u23F1 " + (m > 0 ? m + "m " : "") + s + "s";
1002
+ var zeroDisabled = data.skipZeroCache;
1003
+ document.querySelectorAll("[data-zero-action]").forEach(function(btn) {
1004
+ btn.disabled = zeroDisabled;
1005
+ });
1006
+ }).catch(function() {});
1007
+ }
1008
+
1009
+ function doAction(action, btn) {
1010
+ if (action === "reset-zero") {
1011
+ if (!confirm("Reset zero-cache? This deletes the replica and resyncs from scratch.")) return;
1012
+ }
1013
+ if (action === "reset-zero-full") {
1014
+ if (!confirm("Full reset zero state? This deletes CVR, CDB, and replica databases. Use after schema changes.")) return;
1015
+ }
1016
+ btn.disabled = true;
1017
+ var origText = btn.textContent;
1018
+ btn.textContent = "...";
1019
+ fetch("/api/actions/" + action, { method: "POST" })
1020
+ .then(function(res) { return res.json(); })
1021
+ .then(function(data) {
1022
+ showToast(data.message || "done", data.ok ? "success" : "error");
1023
+ if (action === "clear-logs") {
1024
+ logView.innerHTML = "";
1025
+ lastCursor = 0;
1026
+ }
1027
+ if (action === "clear-http") {
1028
+ document.getElementById("http-body").innerHTML = "";
1029
+ httpCursor = 0;
1030
+ }
1031
+ })
1032
+ .catch(function(err) {
1033
+ showToast("failed: " + err.message, "error");
1034
+ })
1035
+ .finally(function() {
1036
+ btn.disabled = false;
1037
+ btn.textContent = origText;
1038
+ });
1039
+ }
1040
+
1041
+ function showToast(msg, type) {
1042
+ toastEl.textContent = msg;
1043
+ toastEl.className = "toast " + type + " show";
1044
+ setTimeout(function() { toastEl.className = "toast"; }, 2500);
1045
+ }
1046
+
1047
+ // --- data explorer ---
1048
+
1049
+ var isSqlite = false;
1050
+ var browseOffset = 0;
1051
+ var browseTotal = 0;
1052
+ var browseSearch = "";
1053
+ var browseLimit = 100;
1054
+ var lastBrowseFields = [];
1055
+ var lastBrowseRows = [];
1056
+
1057
+ document.getElementById("data-sub-tabs").addEventListener("click", function(e) {
1058
+ var btn = e.target.closest(".data-sub-tab");
1059
+ if (!btn) return;
1060
+ document.querySelectorAll(".data-sub-tab").forEach(function(t) { t.classList.remove("active"); });
1061
+ btn.classList.add("active");
1062
+ dataDb = btn.dataset.db;
1063
+ isSqlite = dataDb === "sqlite";
1064
+ dataActiveTable = null;
1065
+ browseOffset = 0;
1066
+ browseSearch = "";
1067
+ document.getElementById("data-table-search").value = "";
1068
+ document.getElementById("data-paging").style.display = "none";
1069
+ dataResults.innerHTML = '<div class="data-empty">select a table or run a query</div>';
1070
+ sqlEditor.value = "";
1071
+ sqlStatus.textContent = "";
1072
+ loadTables();
1073
+ });
1074
+
1075
+ var tableSearchTimeout = null;
1076
+ document.getElementById("data-table-search").addEventListener("input", function() {
1077
+ clearTimeout(tableSearchTimeout);
1078
+ tableSearchTimeout = setTimeout(renderTableList, 150);
1079
+ });
1080
+
1081
+ function loadTables() {
1082
+ var url = isSqlite ? "/api/sqlite/tables" : "/api/db/tables?db=" + dataDb;
1083
+ fetch(url).then(function(r) { return r.json(); }).then(function(data) {
1084
+ if (data.error) {
1085
+ document.getElementById("data-table-list").innerHTML = '<div style="padding:8px 10px;color:var(--red);font-size:11px">' + escHtml(data.error) + '</div>';
1086
+ return;
1087
+ }
1088
+ dataTables = data.tables || [];
1089
+ renderTableList();
1090
+ }).catch(function() {
1091
+ document.getElementById("data-table-list").innerHTML = '<div style="padding:8px 10px;color:var(--text-dim);font-size:11px">failed to load tables</div>';
1092
+ });
1093
+ }
1094
+
1095
+ function renderTableList() {
1096
+ var filter = (document.getElementById("data-table-search").value || "").toLowerCase();
1097
+ var list = document.getElementById("data-table-list");
1098
+ list.innerHTML = "";
1099
+ for (var i = 0; i < dataTables.length; i++) {
1100
+ var t = dataTables[i];
1101
+ var fullName = isSqlite ? t.name : (t.table_schema === "public" ? t.table_name : t.table_schema + "." + t.table_name);
1102
+ if (filter && fullName.toLowerCase().indexOf(filter) === -1) continue;
1103
+ var div = document.createElement("div");
1104
+ div.className = "data-table-item";
1105
+ if (dataActiveTable === fullName) div.classList.add("active");
1106
+ var sizeText = isSqlite ? (t.col_count + " cols") : fmtSize(t.size_bytes);
1107
+ div.innerHTML = '<span class="tbl-name">' + escHtml(fullName) + '</span><span class="tbl-size">' + sizeText + '</span>';
1108
+ div.dataset.table = fullName;
1109
+ div.addEventListener("click", function() {
1110
+ dataActiveTable = this.dataset.table;
1111
+ document.querySelectorAll(".data-table-item").forEach(function(el) { el.classList.remove("active"); });
1112
+ this.classList.add("active");
1113
+ browseOffset = 0;
1114
+ browseSearch = "";
1115
+ browseTableData();
1116
+ });
1117
+ list.appendChild(div);
1118
+ }
1119
+ }
1120
+
1121
+ function browseTableData() {
1122
+ if (!dataActiveTable) return;
1123
+ var baseUrl = isSqlite ? "/api/sqlite/table-data" : "/api/db/table-data";
1124
+ var params = new URLSearchParams();
1125
+ if (!isSqlite) params.set("db", dataDb);
1126
+ params.set("table", dataActiveTable);
1127
+ params.set("offset", String(browseOffset));
1128
+ params.set("limit", String(browseLimit));
1129
+ if (browseSearch) params.set("search", browseSearch);
1130
+ sqlStatus.textContent = "loading...";
1131
+ sqlStatus.className = "sql-status";
1132
+ sqlEditor.value = "";
1133
+ fetch(baseUrl + "?" + params).then(function(r) { return r.json(); }).then(function(data) {
1134
+ if (data.error) {
1135
+ sqlStatus.textContent = data.error;
1136
+ sqlStatus.className = "sql-status error";
1137
+ return;
1138
+ }
1139
+ browseTotal = data.total;
1140
+ var cols = data.columns || [];
1141
+ var fields = cols.map(function(c) { return c.name; });
1142
+ lastBrowseFields = fields;
1143
+ lastBrowseRows = data.rows;
1144
+ sqlStatus.textContent = data.total + " total row" + (data.total !== 1 ? "s" : "");
1145
+ sqlStatus.className = "sql-status";
1146
+ renderBrowseResults(fields, data.rows);
1147
+ updatePaging();
1148
+ }).catch(function(err) {
1149
+ sqlStatus.textContent = err.message;
1150
+ sqlStatus.className = "sql-status error";
1151
+ });
1152
+ }
1153
+
1154
+ function browseTable(dir) {
1155
+ browseOffset = Math.max(0, browseOffset + dir * browseLimit);
1156
+ browseTableData();
1157
+ }
1158
+
1159
+ function updatePaging() {
1160
+ var paging = document.getElementById("data-paging");
1161
+ if (browseTotal <= browseLimit && browseOffset === 0) {
1162
+ paging.style.display = "none";
1163
+ return;
1164
+ }
1165
+ paging.style.display = "flex";
1166
+ var from = browseOffset + 1;
1167
+ var to = Math.min(browseOffset + browseLimit, browseTotal);
1168
+ document.getElementById("data-paging-info").textContent = from + "-" + to + " of " + browseTotal;
1169
+ document.getElementById("data-prev-btn").disabled = browseOffset === 0;
1170
+ document.getElementById("data-next-btn").disabled = browseOffset + browseLimit >= browseTotal;
1171
+ }
1172
+
1173
+ function quoteIdent(name) {
1174
+ if (name.indexOf(".") > -1) {
1175
+ var parts = name.split(".");
1176
+ return '"' + parts[0] + '"."' + parts[1] + '"';
1177
+ }
1178
+ if (/^[a-z_][a-z0-9_]*$/.test(name)) return name;
1179
+ return '"' + name + '"';
1180
+ }
1181
+
1182
+ function runSql() {
1183
+ var sql = sqlEditor.value.trim();
1184
+ if (!sql) return;
1185
+ var btn = document.getElementById("sql-run-btn");
1186
+ btn.disabled = true;
1187
+ sqlStatus.textContent = "running...";
1188
+ sqlStatus.className = "sql-status";
1189
+ document.getElementById("data-paging").style.display = "none";
1190
+ var endpoint = isSqlite ? "/api/sqlite/query" : "/api/db/query";
1191
+ var body = isSqlite ? { sql: sql } : { db: dataDb, sql: sql };
1192
+ fetch(endpoint, {
1193
+ method: "POST",
1194
+ headers: { "Content-Type": "application/json" },
1195
+ body: JSON.stringify(body)
1196
+ }).then(function(r) { return r.json(); }).then(function(data) {
1197
+ btn.disabled = false;
1198
+ if (data.error) {
1199
+ sqlStatus.textContent = data.error;
1200
+ sqlStatus.className = "sql-status error";
1201
+ return;
1202
+ }
1203
+ sqlStatus.textContent = data.rowCount + " row" + (data.rowCount !== 1 ? "s" : "") + " in " + data.durationMs + "ms";
1204
+ sqlStatus.className = "sql-status";
1205
+ lastBrowseFields = data.fields;
1206
+ lastBrowseRows = data.rows;
1207
+ renderBrowseResults(data.fields, data.rows);
1208
+ }).catch(function(err) {
1209
+ btn.disabled = false;
1210
+ sqlStatus.textContent = err.message;
1211
+ sqlStatus.className = "sql-status error";
1212
+ });
1213
+ }
1214
+
1215
+ function renderBrowseResults(fields, rows) {
1216
+ if (!fields || fields.length === 0) {
1217
+ dataResults.innerHTML = '<div class="data-empty">no columns returned</div>';
1218
+ return;
1219
+ }
1220
+ var html = '<table><thead><tr>';
1221
+ for (var i = 0; i < fields.length; i++) {
1222
+ html += '<th>' + escHtml(fields[i]) + '</th>';
1223
+ }
1224
+ html += '</tr></thead><tbody>';
1225
+ for (var r = 0; r < rows.length; r++) {
1226
+ html += '<tr class="clickable" data-row-idx="' + r + '">';
1227
+ for (var c = 0; c < fields.length; c++) {
1228
+ var val = rows[r][fields[c]];
1229
+ if (val === null || val === undefined) {
1230
+ html += '<td class="null-val">null</td>';
1231
+ } else if (typeof val === "object") {
1232
+ html += '<td>' + escHtml(JSON.stringify(val)) + '</td>';
1233
+ } else {
1234
+ var s = String(val);
1235
+ html += '<td>' + escHtml(s.length > 120 ? s.slice(0, 120) + "..." : s) + '</td>';
1236
+ }
1237
+ }
1238
+ html += '</tr>';
1239
+ }
1240
+ html += '</tbody></table>';
1241
+ dataResults.innerHTML = html;
1242
+ // click rows to open detail
1243
+ dataResults.querySelectorAll("tr.clickable").forEach(function(tr) {
1244
+ tr.addEventListener("click", function() {
1245
+ var idx = Number(this.dataset.rowIdx);
1246
+ openRowDetail(lastBrowseFields, lastBrowseRows[idx]);
1247
+ });
1248
+ });
1249
+ }
1250
+
1251
+ function openRowDetail(fields, row) {
1252
+ if (!row) return;
1253
+ var body = document.getElementById("row-detail-body");
1254
+ var html = "";
1255
+ for (var i = 0; i < fields.length; i++) {
1256
+ var val = row[fields[i]];
1257
+ var valStr;
1258
+ var cls = "row-detail-val";
1259
+ if (val === null || val === undefined) {
1260
+ valStr = "null";
1261
+ cls += " null-val";
1262
+ } else if (typeof val === "object") {
1263
+ valStr = JSON.stringify(val, null, 2);
1264
+ } else {
1265
+ valStr = String(val);
1266
+ }
1267
+ html += '<div class="row-detail-field">';
1268
+ html += '<div class="row-detail-key">' + escHtml(fields[i]) + '</div>';
1269
+ html += '<div class="' + cls + '">' + escHtml(valStr) + '</div>';
1270
+ html += '</div>';
1271
+ }
1272
+ body.innerHTML = html;
1273
+ document.getElementById("row-detail-overlay").classList.add("open");
1274
+ }
1275
+
1276
+ function closeRowDetail() {
1277
+ document.getElementById("row-detail-overlay").classList.remove("open");
1278
+ }
1279
+
1280
+ document.getElementById("row-detail-overlay").addEventListener("click", function(e) {
1281
+ if (e.target === this) closeRowDetail();
1282
+ });
1283
+
1284
+ document.addEventListener("keydown", function(e) {
1285
+ if (e.key === "Escape") closeRowDetail();
1286
+ });
1287
+
1288
+ // cmd+enter / ctrl+enter to run sql
1289
+ sqlEditor.addEventListener("keydown", function(e) {
1290
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1291
+ e.preventDefault();
1292
+ runSql();
1293
+ }
1294
+ });
1295
+
1296
+ // --- polling ---
1297
+
1298
+ fetchStatus();
1299
+ setInterval(function() {
1300
+ if (document.hidden) return;
1301
+ if (isDataTab) return;
1302
+ if (isHttpTab) fetchHttp();
1303
+ else if (!isEnvTab) fetchLogs();
1304
+ }, 1000);
1305
+ setInterval(function() { if (!document.hidden) fetchStatus(); }, 5000);
1306
+ document.addEventListener("visibilitychange", function() {
1307
+ if (document.hidden) return;
1308
+ if (isDataTab) return;
1309
+ if (isHttpTab) fetchHttp();
1310
+ else if (!isEnvTab) fetchLogs();
1311
+ fetchStatus();
1312
+ });
1313
+ window.addEventListener("popstate", function() {
1314
+ var p = window.location.pathname.replace(/\\/$/, "") || "/";
1315
+ var s = pathMap[p] !== undefined ? pathMap[p] : "data";
1316
+ var tab = document.querySelector('#tab-bar .tab[data-source="' + s + '"]');
1317
+ if (tab) tab.click();
1318
+ });
1319
+ </script>
1320
+ </body>
1321
+ </html>`;
727
1322
  }
728
1323
  //# sourceMappingURL=ui.js.map