@fugood/buttress-server-poc 2.23.0-beta.28 → 2.23.0-beta.29
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/bin/start.mjs +3 -1
- package/lib/index.js +1 -1
- package/package.json +5 -4
- package/public/status.html +896 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Buttress Server Status</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #1a1a2e;
|
|
10
|
+
--bg-secondary: #16213e;
|
|
11
|
+
--bg-card: #1f2940;
|
|
12
|
+
--text: #e6e6e6;
|
|
13
|
+
--text-muted: #8892a8;
|
|
14
|
+
--accent: #4a90d9;
|
|
15
|
+
--success: #4caf50;
|
|
16
|
+
--warning: #ff9800;
|
|
17
|
+
--error: #f44336;
|
|
18
|
+
--border: #2d3a50;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
padding: 16px;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
-webkit-text-size-adjust: 100%;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.container {
|
|
38
|
+
max-width: 1400px;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
overflow-x: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
header {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
margin-bottom: 20px;
|
|
48
|
+
padding-bottom: 16px;
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.header-top {
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.header-bottom {
|
|
59
|
+
display: flex;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
align-items: center;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
h1 {
|
|
67
|
+
font-size: 20px;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.connection-status {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
font-size: 13px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.status-dot {
|
|
79
|
+
width: 8px;
|
|
80
|
+
height: 8px;
|
|
81
|
+
border-radius: 50%;
|
|
82
|
+
background: var(--text-muted);
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.status-dot.connected { background: var(--success); }
|
|
87
|
+
.status-dot.connecting { background: var(--warning); animation: pulse 1s infinite; }
|
|
88
|
+
.status-dot.error { background: var(--error); }
|
|
89
|
+
|
|
90
|
+
@keyframes pulse {
|
|
91
|
+
0%, 100% { opacity: 1; }
|
|
92
|
+
50% { opacity: 0.5; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.grid {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: 1fr;
|
|
98
|
+
gap: 16px;
|
|
99
|
+
max-width: 100%;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.card {
|
|
103
|
+
background: var(--bg-card);
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
padding: 16px;
|
|
106
|
+
border: 1px solid var(--border);
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.card-header {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
align-items: center;
|
|
115
|
+
margin-bottom: 12px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.card-title {
|
|
119
|
+
font-size: 15px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: var(--accent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.badge {
|
|
125
|
+
padding: 3px 6px;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
font-size: 11px;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.badge-success { background: rgba(76, 175, 80, 0.2); color: var(--success); }
|
|
133
|
+
.badge-warning { background: rgba(255, 152, 0, 0.2); color: var(--warning); }
|
|
134
|
+
.badge-error { background: rgba(244, 67, 54, 0.2); color: var(--error); }
|
|
135
|
+
.badge-info { background: rgba(74, 144, 217, 0.2); color: var(--accent); }
|
|
136
|
+
|
|
137
|
+
.table-wrapper {
|
|
138
|
+
overflow-x: auto;
|
|
139
|
+
-webkit-overflow-scrolling: touch;
|
|
140
|
+
scrollbar-width: thin;
|
|
141
|
+
scrollbar-color: var(--border) transparent;
|
|
142
|
+
padding-bottom: 8px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.table-wrapper::-webkit-scrollbar {
|
|
146
|
+
height: 6px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.table-wrapper::-webkit-scrollbar-track {
|
|
150
|
+
background: transparent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.table-wrapper::-webkit-scrollbar-thumb {
|
|
154
|
+
background: var(--border);
|
|
155
|
+
border-radius: 3px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.table-inner {
|
|
159
|
+
display: inline-block;
|
|
160
|
+
min-width: 100%;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
table {
|
|
164
|
+
border-collapse: collapse;
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
white-space: nowrap;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
th, td {
|
|
170
|
+
text-align: left;
|
|
171
|
+
padding: 8px 10px;
|
|
172
|
+
border-bottom: 1px solid var(--border);
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
th {
|
|
177
|
+
font-weight: 500;
|
|
178
|
+
color: var(--text-muted);
|
|
179
|
+
font-size: 11px;
|
|
180
|
+
text-transform: uppercase;
|
|
181
|
+
letter-spacing: 0.5px;
|
|
182
|
+
position: sticky;
|
|
183
|
+
top: 0;
|
|
184
|
+
background: var(--bg-card);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
tr:active td {
|
|
188
|
+
background: rgba(255, 255, 255, 0.05);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.empty-state {
|
|
192
|
+
text-align: center;
|
|
193
|
+
padding: 24px 16px;
|
|
194
|
+
color: var(--text-muted);
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.section {
|
|
199
|
+
margin-bottom: 16px;
|
|
200
|
+
min-width: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.section-title {
|
|
204
|
+
font-size: 12px;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
color: var(--text-muted);
|
|
207
|
+
margin-bottom: 10px;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 0.5px;
|
|
210
|
+
padding: 8px 0;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
-webkit-tap-highlight-color: transparent;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.collapsible {
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
user-select: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.collapsible::before {
|
|
222
|
+
content: '▼';
|
|
223
|
+
display: inline-block;
|
|
224
|
+
margin-right: 8px;
|
|
225
|
+
font-size: 10px;
|
|
226
|
+
transition: transform 0.2s;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.collapsible.collapsed::before {
|
|
230
|
+
transform: rotate(-90deg);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.collapsible-content {
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
transition: max-height 0.3s;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.collapsible-content.collapsed {
|
|
239
|
+
max-height: 0 !important;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.stat-grid {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: repeat(3, 1fr);
|
|
245
|
+
gap: 8px;
|
|
246
|
+
margin-bottom: 16px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.stat-item {
|
|
250
|
+
background: var(--bg-secondary);
|
|
251
|
+
padding: 10px 8px;
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
text-align: center;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.stat-value {
|
|
257
|
+
font-size: 20px;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
color: var(--accent);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.stat-label {
|
|
263
|
+
font-size: 10px;
|
|
264
|
+
color: var(--text-muted);
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
margin-top: 2px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.request-state {
|
|
270
|
+
display: inline-block;
|
|
271
|
+
padding: 2px 5px;
|
|
272
|
+
border-radius: 3px;
|
|
273
|
+
font-size: 10px;
|
|
274
|
+
font-weight: 500;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.request-state.queued { background: rgba(74, 144, 217, 0.2); color: var(--accent); }
|
|
278
|
+
.request-state.processing_prompt { background: rgba(255, 152, 0, 0.2); color: var(--warning); }
|
|
279
|
+
.request-state.generating { background: rgba(76, 175, 80, 0.2); color: var(--success); }
|
|
280
|
+
.request-state.done { background: rgba(128, 128, 128, 0.2); color: var(--text-muted); }
|
|
281
|
+
|
|
282
|
+
.timestamp {
|
|
283
|
+
color: var(--text-muted);
|
|
284
|
+
font-size: 11px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.mono {
|
|
288
|
+
font-family: 'SF Mono', 'Menlo', monospace;
|
|
289
|
+
font-size: 11px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.refresh-btn {
|
|
293
|
+
background: var(--accent);
|
|
294
|
+
color: white;
|
|
295
|
+
border: none;
|
|
296
|
+
padding: 10px 16px;
|
|
297
|
+
border-radius: 6px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
font-size: 13px;
|
|
300
|
+
font-weight: 500;
|
|
301
|
+
-webkit-tap-highlight-color: transparent;
|
|
302
|
+
touch-action: manipulation;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.refresh-btn:active {
|
|
306
|
+
opacity: 0.8;
|
|
307
|
+
transform: scale(0.98);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.last-update {
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
color: var(--text-muted);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Tablet and up */
|
|
316
|
+
@media (min-width: 768px) {
|
|
317
|
+
body {
|
|
318
|
+
padding: 20px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
header {
|
|
322
|
+
flex-direction: row;
|
|
323
|
+
justify-content: space-between;
|
|
324
|
+
align-items: center;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.header-top {
|
|
328
|
+
flex: 1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.header-bottom {
|
|
332
|
+
flex: none;
|
|
333
|
+
gap: 16px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
h1 {
|
|
337
|
+
font-size: 24px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.grid {
|
|
341
|
+
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
|
342
|
+
gap: 20px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.card {
|
|
346
|
+
padding: 20px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
table {
|
|
350
|
+
font-size: 13px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
th, td {
|
|
354
|
+
padding: 10px 12px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.stat-value {
|
|
358
|
+
font-size: 24px;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.stat-label {
|
|
362
|
+
font-size: 11px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.table-wrapper {
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Large screens */
|
|
372
|
+
@media (min-width: 1200px) {
|
|
373
|
+
.grid {
|
|
374
|
+
grid-template-columns: repeat(2, 1fr);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
</style>
|
|
378
|
+
</head>
|
|
379
|
+
<body>
|
|
380
|
+
<div class="container">
|
|
381
|
+
<header>
|
|
382
|
+
<div class="header-top">
|
|
383
|
+
<h1>Buttress Status</h1>
|
|
384
|
+
<div class="connection-status">
|
|
385
|
+
<span class="status-dot" id="connectionDot"></span>
|
|
386
|
+
<span id="connectionText">Connecting...</span>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="header-bottom">
|
|
390
|
+
<span class="last-update" id="lastUpdate">-</span>
|
|
391
|
+
<button class="refresh-btn" onclick="refreshStatus()">Refresh</button>
|
|
392
|
+
</div>
|
|
393
|
+
</header>
|
|
394
|
+
|
|
395
|
+
<div class="grid">
|
|
396
|
+
<!-- LLM Status -->
|
|
397
|
+
<div class="card">
|
|
398
|
+
<div class="card-header">
|
|
399
|
+
<span class="card-title">GGML-LLM Status</span>
|
|
400
|
+
<span class="badge badge-info" id="llmGeneratorCount">0 generators</span>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<div id="llmGenerators"></div>
|
|
404
|
+
|
|
405
|
+
<div class="section">
|
|
406
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Parallel Status</div>
|
|
407
|
+
<div class="collapsible-content" id="llmParallelStatus">
|
|
408
|
+
<div class="empty-state">No active parallel requests</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div class="section">
|
|
413
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
|
|
414
|
+
<div class="collapsible-content" id="llmModelHistory">
|
|
415
|
+
<div class="empty-state">No model load history</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="section">
|
|
420
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Completion History</div>
|
|
421
|
+
<div class="collapsible-content" id="llmCompletionHistory">
|
|
422
|
+
<div class="empty-state">No completion history</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<!-- STT Status -->
|
|
428
|
+
<div class="card">
|
|
429
|
+
<div class="card-header">
|
|
430
|
+
<span class="card-title">GGML-STT Status</span>
|
|
431
|
+
<span class="badge badge-info" id="sttGeneratorCount">0 generators</span>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div id="sttGenerators"></div>
|
|
435
|
+
|
|
436
|
+
<div class="section">
|
|
437
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Queue Status</div>
|
|
438
|
+
<div class="collapsible-content" id="sttQueueStatus">
|
|
439
|
+
<div class="empty-state">No queue info</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<div class="section">
|
|
444
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
|
|
445
|
+
<div class="collapsible-content" id="sttModelHistory">
|
|
446
|
+
<div class="empty-state">No model load history</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<div class="section">
|
|
451
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Transcription History</div>
|
|
452
|
+
<div class="collapsible-content" id="sttTranscriptionHistory">
|
|
453
|
+
<div class="empty-state">No transcription history</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<script>
|
|
461
|
+
// State
|
|
462
|
+
let eventSource = null
|
|
463
|
+
let reconnectTimeout = null
|
|
464
|
+
let reconnectAttempts = 0
|
|
465
|
+
const MAX_RECONNECT_ATTEMPTS = 10
|
|
466
|
+
const RECONNECT_DELAY = 3000
|
|
467
|
+
|
|
468
|
+
// Relative time formatting
|
|
469
|
+
function formatRelativeTime(timestamp) {
|
|
470
|
+
const now = Date.now()
|
|
471
|
+
const then = new Date(timestamp).getTime()
|
|
472
|
+
const diff = now - then
|
|
473
|
+
|
|
474
|
+
if (diff < 1000) return 'just now'
|
|
475
|
+
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`
|
|
476
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
|
|
477
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
|
|
478
|
+
return new Date(timestamp).toLocaleString()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Toggle collapsible sections
|
|
482
|
+
function toggleSection(el) {
|
|
483
|
+
el.classList.toggle('collapsed')
|
|
484
|
+
el.nextElementSibling.classList.toggle('collapsed')
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Update connection status
|
|
488
|
+
function setConnectionStatus(status) {
|
|
489
|
+
const dot = document.getElementById('connectionDot')
|
|
490
|
+
const text = document.getElementById('connectionText')
|
|
491
|
+
dot.className = 'status-dot ' + status
|
|
492
|
+
text.textContent = status === 'connected' ? 'Subscribed (Live)' :
|
|
493
|
+
status === 'connecting' ? 'Connecting...' : 'Disconnected'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Render LLM generators
|
|
497
|
+
function renderLlmGenerators(generators) {
|
|
498
|
+
const container = document.getElementById('llmGenerators')
|
|
499
|
+
document.getElementById('llmGeneratorCount').textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
|
|
500
|
+
|
|
501
|
+
if (generators.length === 0) {
|
|
502
|
+
container.innerHTML = '<div class="empty-state">No LLM generators loaded</div>'
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
container.innerHTML = `
|
|
507
|
+
<div class="table-wrapper">
|
|
508
|
+
<div class="table-inner">
|
|
509
|
+
<table>
|
|
510
|
+
<thead>
|
|
511
|
+
<tr>
|
|
512
|
+
<th>ID</th>
|
|
513
|
+
<th>Repo</th>
|
|
514
|
+
<th>Quantization</th>
|
|
515
|
+
<th>Variant</th>
|
|
516
|
+
<th>Context</th>
|
|
517
|
+
<th>Parallel</th>
|
|
518
|
+
</tr>
|
|
519
|
+
</thead>
|
|
520
|
+
<tbody>
|
|
521
|
+
${generators.map(g => `
|
|
522
|
+
<tr>
|
|
523
|
+
<td class="mono">${escapeHtml(g.id?.slice(0, 20) || '-')}...</td>
|
|
524
|
+
<td>${escapeHtml(g.repoId || '-')}</td>
|
|
525
|
+
<td>${escapeHtml(g.quantization || '-')}</td>
|
|
526
|
+
<td>${escapeHtml(g.variant || '-')}</td>
|
|
527
|
+
<td>${g.nCtx?.toLocaleString() || '-'}</td>
|
|
528
|
+
<td>${g.nParallel || '-'}</td>
|
|
529
|
+
</tr>
|
|
530
|
+
`).join('')}
|
|
531
|
+
</tbody>
|
|
532
|
+
</table>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
`
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Render parallel statuses
|
|
539
|
+
function renderParallelStatus(statuses) {
|
|
540
|
+
const container = document.getElementById('llmParallelStatus')
|
|
541
|
+
|
|
542
|
+
if (!statuses || statuses.length === 0) {
|
|
543
|
+
container.innerHTML = '<div class="empty-state">No active parallel requests</div>'
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let html = ''
|
|
548
|
+
for (const status of statuses) {
|
|
549
|
+
html += `
|
|
550
|
+
<div class="stat-grid">
|
|
551
|
+
<div class="stat-item">
|
|
552
|
+
<div class="stat-value">${status.n_parallel || 0}</div>
|
|
553
|
+
<div class="stat-label">Parallel Slots</div>
|
|
554
|
+
</div>
|
|
555
|
+
<div class="stat-item">
|
|
556
|
+
<div class="stat-value">${status.active_slots || 0}</div>
|
|
557
|
+
<div class="stat-label">Active</div>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="stat-item">
|
|
560
|
+
<div class="stat-value">${status.queued_requests || 0}</div>
|
|
561
|
+
<div class="stat-label">Queued</div>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
`
|
|
565
|
+
|
|
566
|
+
if (status.requests && status.requests.length > 0) {
|
|
567
|
+
html += `
|
|
568
|
+
<div class="table-wrapper">
|
|
569
|
+
<div class="table-inner">
|
|
570
|
+
<table>
|
|
571
|
+
<thead>
|
|
572
|
+
<tr>
|
|
573
|
+
<th>Request ID</th>
|
|
574
|
+
<th>Type</th>
|
|
575
|
+
<th>State</th>
|
|
576
|
+
<th>Tokens</th>
|
|
577
|
+
<th>Speed</th>
|
|
578
|
+
</tr>
|
|
579
|
+
</thead>
|
|
580
|
+
<tbody>
|
|
581
|
+
${status.requests.map(r => `
|
|
582
|
+
<tr>
|
|
583
|
+
<td class="mono">${r.request_id}</td>
|
|
584
|
+
<td>${r.type}</td>
|
|
585
|
+
<td><span class="request-state ${r.state}">${r.state}</span></td>
|
|
586
|
+
<td>${r.tokens_generated || 0}</td>
|
|
587
|
+
<td>${r.tokens_per_second?.toFixed(1) || '-'} t/s</td>
|
|
588
|
+
</tr>
|
|
589
|
+
`).join('')}
|
|
590
|
+
</tbody>
|
|
591
|
+
</table>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
`
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
container.innerHTML = html || '<div class="empty-state">No parallel status available</div>'
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Render history tables
|
|
602
|
+
function renderHistory(containerId, items, columns) {
|
|
603
|
+
const container = document.getElementById(containerId)
|
|
604
|
+
|
|
605
|
+
if (!items || items.length === 0) {
|
|
606
|
+
container.innerHTML = `<div class="empty-state">No history</div>`
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
container.innerHTML = `
|
|
611
|
+
<div class="table-wrapper">
|
|
612
|
+
<div class="table-inner">
|
|
613
|
+
<table>
|
|
614
|
+
<thead>
|
|
615
|
+
<tr>
|
|
616
|
+
${columns.map(c => `<th>${c.label}</th>`).join('')}
|
|
617
|
+
</tr>
|
|
618
|
+
</thead>
|
|
619
|
+
<tbody>
|
|
620
|
+
${items.slice(0, 20).map(item => `
|
|
621
|
+
<tr>
|
|
622
|
+
${columns.map(c => `<td>${c.render(item)}</td>`).join('')}
|
|
623
|
+
</tr>
|
|
624
|
+
`).join('')}
|
|
625
|
+
</tbody>
|
|
626
|
+
</table>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
`
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Render STT generators
|
|
633
|
+
function renderSttGenerators(generators) {
|
|
634
|
+
const container = document.getElementById('sttGenerators')
|
|
635
|
+
document.getElementById('sttGeneratorCount').textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
|
|
636
|
+
|
|
637
|
+
if (generators.length === 0) {
|
|
638
|
+
container.innerHTML = '<div class="empty-state">No STT generators loaded</div>'
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
container.innerHTML = `
|
|
643
|
+
<div class="table-wrapper">
|
|
644
|
+
<div class="table-inner">
|
|
645
|
+
<table>
|
|
646
|
+
<thead>
|
|
647
|
+
<tr>
|
|
648
|
+
<th>ID</th>
|
|
649
|
+
<th>Repo</th>
|
|
650
|
+
<th>Quantization</th>
|
|
651
|
+
<th>Variant</th>
|
|
652
|
+
<th>Context</th>
|
|
653
|
+
</tr>
|
|
654
|
+
</thead>
|
|
655
|
+
<tbody>
|
|
656
|
+
${generators.map(g => `
|
|
657
|
+
<tr>
|
|
658
|
+
<td class="mono">${escapeHtml(g.id?.slice(0, 20) || '-')}...</td>
|
|
659
|
+
<td>${escapeHtml(g.repoId || '-')}</td>
|
|
660
|
+
<td>${escapeHtml(g.quantization || '-')}</td>
|
|
661
|
+
<td>${escapeHtml(g.variant || '-')}</td>
|
|
662
|
+
<td>${g.hasContext ? '<span class="badge badge-success">Active</span>' : '<span class="badge badge-warning">Inactive</span>'}</td>
|
|
663
|
+
</tr>
|
|
664
|
+
`).join('')}
|
|
665
|
+
</tbody>
|
|
666
|
+
</table>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
`
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Render STT queue status
|
|
673
|
+
function renderSttQueueStatus(generators) {
|
|
674
|
+
const container = document.getElementById('sttQueueStatus')
|
|
675
|
+
const queues = generators.filter(g => g.queueStatus)
|
|
676
|
+
|
|
677
|
+
if (queues.length === 0) {
|
|
678
|
+
container.innerHTML = '<div class="empty-state">No queue info available</div>'
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
container.innerHTML = `
|
|
683
|
+
<div class="stat-grid">
|
|
684
|
+
${queues.map(g => `
|
|
685
|
+
<div class="stat-item">
|
|
686
|
+
<div class="stat-value">${g.queueStatus.processing ? '1' : '0'}</div>
|
|
687
|
+
<div class="stat-label">Processing</div>
|
|
688
|
+
</div>
|
|
689
|
+
<div class="stat-item">
|
|
690
|
+
<div class="stat-value">${g.queueStatus.queuedCount || 0}</div>
|
|
691
|
+
<div class="stat-label">Queued</div>
|
|
692
|
+
</div>
|
|
693
|
+
`).join('')}
|
|
694
|
+
</div>
|
|
695
|
+
`
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Escape HTML
|
|
699
|
+
function escapeHtml(str) {
|
|
700
|
+
if (str == null) return ''
|
|
701
|
+
return String(str)
|
|
702
|
+
.replace(/&/g, '&')
|
|
703
|
+
.replace(/</g, '<')
|
|
704
|
+
.replace(/>/g, '>')
|
|
705
|
+
.replace(/"/g, '"')
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Update the UI with status data
|
|
709
|
+
function updateUI(status) {
|
|
710
|
+
document.getElementById('lastUpdate').textContent =
|
|
711
|
+
'Updated ' + formatRelativeTime(status.timestamp || new Date().toISOString())
|
|
712
|
+
|
|
713
|
+
const llm = status.ggmlLlm || {}
|
|
714
|
+
const stt = status.ggmlStt || {}
|
|
715
|
+
|
|
716
|
+
// LLM
|
|
717
|
+
renderLlmGenerators(llm.generators || [])
|
|
718
|
+
renderParallelStatus(llm.parallelStatuses || [])
|
|
719
|
+
|
|
720
|
+
renderHistory('llmModelHistory', llm.history?.modelLoads || [], [
|
|
721
|
+
{ label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
|
|
722
|
+
{ label: 'Repo', render: i => escapeHtml(i.repoId || '-') },
|
|
723
|
+
{ label: 'Quantization', render: i => escapeHtml(i.quantization || '-') },
|
|
724
|
+
{ label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
|
|
725
|
+
{ label: 'Status', render: i => i.success ?
|
|
726
|
+
'<span class="badge badge-success">Success</span>' :
|
|
727
|
+
`<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>` },
|
|
728
|
+
])
|
|
729
|
+
|
|
730
|
+
renderHistory('llmCompletionHistory', llm.history?.completions || [], [
|
|
731
|
+
{ label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
|
|
732
|
+
{ label: 'Tokens', render: i => `${i.tokensGenerated || 0}` },
|
|
733
|
+
{ label: 'Speed', render: i => `${i.tokensPerSecond?.toFixed(1) || '-'} t/s` },
|
|
734
|
+
{ label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
|
|
735
|
+
{ label: 'Status', render: i => i.success ?
|
|
736
|
+
(i.interrupted ? '<span class="badge badge-warning">Interrupted</span>' : '<span class="badge badge-success">Success</span>') :
|
|
737
|
+
`<span class="badge badge-error">Failed</span>` },
|
|
738
|
+
])
|
|
739
|
+
|
|
740
|
+
// STT
|
|
741
|
+
renderSttGenerators(stt.generators || [])
|
|
742
|
+
renderSttQueueStatus(stt.generators || [])
|
|
743
|
+
|
|
744
|
+
renderHistory('sttModelHistory', stt.history?.modelLoads || [], [
|
|
745
|
+
{ label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
|
|
746
|
+
{ label: 'Repo', render: i => escapeHtml(i.repoId || '-') },
|
|
747
|
+
{ label: 'Quantization', render: i => escapeHtml(i.quantization || '-') },
|
|
748
|
+
{ label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
|
|
749
|
+
{ label: 'Status', render: i => i.success ?
|
|
750
|
+
'<span class="badge badge-success">Success</span>' :
|
|
751
|
+
`<span class="badge badge-error">Failed</span>` },
|
|
752
|
+
])
|
|
753
|
+
|
|
754
|
+
renderHistory('sttTranscriptionHistory', stt.history?.transcriptions || [], [
|
|
755
|
+
{ label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
|
|
756
|
+
{ label: 'Segments', render: i => i.segmentCount || '-' },
|
|
757
|
+
{ label: 'Text Length', render: i => i.textLength || '-' },
|
|
758
|
+
{ label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
|
|
759
|
+
{ label: 'Status', render: i => i.success ?
|
|
760
|
+
'<span class="badge badge-success">Success</span>' :
|
|
761
|
+
`<span class="badge badge-error">Failed</span>` },
|
|
762
|
+
])
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Parse tRPC SSE response
|
|
766
|
+
function parseTrpcSseData(data) {
|
|
767
|
+
try {
|
|
768
|
+
const parsed = JSON.parse(data)
|
|
769
|
+
// tRPC wraps data in result.data
|
|
770
|
+
if (parsed.result?.data) {
|
|
771
|
+
return parsed.result.data
|
|
772
|
+
}
|
|
773
|
+
return parsed
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.error('Failed to parse SSE data:', e)
|
|
776
|
+
return null
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Subscribe to status updates via SSE (tRPC subscription)
|
|
781
|
+
function subscribeToStatus() {
|
|
782
|
+
if (eventSource) {
|
|
783
|
+
eventSource.close()
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
setConnectionStatus('connecting')
|
|
787
|
+
console.log('[Status] Connecting to subscription...')
|
|
788
|
+
|
|
789
|
+
// tRPC subscription URL format
|
|
790
|
+
const subscriptionUrl = '/trpc/status.subscribe'
|
|
791
|
+
|
|
792
|
+
eventSource = new EventSource(subscriptionUrl)
|
|
793
|
+
|
|
794
|
+
eventSource.onopen = () => {
|
|
795
|
+
console.log('[Status] Subscription connected')
|
|
796
|
+
setConnectionStatus('connected')
|
|
797
|
+
reconnectAttempts = 0
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
eventSource.onmessage = (event) => {
|
|
801
|
+
const data = parseTrpcSseData(event.data)
|
|
802
|
+
if (!data) return
|
|
803
|
+
|
|
804
|
+
console.log('[Status] Received update:', data.type)
|
|
805
|
+
|
|
806
|
+
// Handle different message types
|
|
807
|
+
if (data.type === 'initial' && data.data) {
|
|
808
|
+
updateUI(data.data)
|
|
809
|
+
} else if (data.type === 'change' && data.fullStatus) {
|
|
810
|
+
updateUI(data.fullStatus)
|
|
811
|
+
} else if (data.type === 'parallelStatus' && data.fullStatus) {
|
|
812
|
+
// Parallel status update - refresh full UI
|
|
813
|
+
updateUI(data.fullStatus)
|
|
814
|
+
} else if (data.fullStatus) {
|
|
815
|
+
updateUI(data.fullStatus)
|
|
816
|
+
} else if (data.ggmlLlm || data.ggmlStt) {
|
|
817
|
+
// Direct status object
|
|
818
|
+
updateUI(data)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
eventSource.onerror = (error) => {
|
|
823
|
+
console.error('[Status] Subscription error:', error)
|
|
824
|
+
setConnectionStatus('error')
|
|
825
|
+
eventSource.close()
|
|
826
|
+
eventSource = null
|
|
827
|
+
|
|
828
|
+
// Attempt to reconnect
|
|
829
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
830
|
+
reconnectAttempts++
|
|
831
|
+
console.log(`[Status] Reconnecting in ${RECONNECT_DELAY}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`)
|
|
832
|
+
reconnectTimeout = setTimeout(subscribeToStatus, RECONNECT_DELAY)
|
|
833
|
+
} else {
|
|
834
|
+
console.log('[Status] Max reconnect attempts reached, falling back to polling')
|
|
835
|
+
startPolling()
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Fallback: Fetch status via HTTP polling
|
|
841
|
+
let pollingInterval = null
|
|
842
|
+
|
|
843
|
+
async function fetchStatus() {
|
|
844
|
+
try {
|
|
845
|
+
const response = await fetch('/trpc/status.getStatus')
|
|
846
|
+
const data = await response.json()
|
|
847
|
+
if (data.result?.data) {
|
|
848
|
+
updateUI(data.result.data)
|
|
849
|
+
setConnectionStatus('connected')
|
|
850
|
+
}
|
|
851
|
+
} catch (error) {
|
|
852
|
+
console.error('Failed to fetch status:', error)
|
|
853
|
+
setConnectionStatus('error')
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function startPolling() {
|
|
858
|
+
console.log('[Status] Starting polling fallback')
|
|
859
|
+
if (pollingInterval) clearInterval(pollingInterval)
|
|
860
|
+
fetchStatus()
|
|
861
|
+
pollingInterval = setInterval(fetchStatus, 3000)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function stopPolling() {
|
|
865
|
+
if (pollingInterval) {
|
|
866
|
+
clearInterval(pollingInterval)
|
|
867
|
+
pollingInterval = null
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Manual refresh button
|
|
872
|
+
async function refreshStatus() {
|
|
873
|
+
await fetchStatus()
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Initialize: Try subscription first, fallback to polling
|
|
877
|
+
function init() {
|
|
878
|
+
// Try subscription
|
|
879
|
+
subscribeToStatus()
|
|
880
|
+
|
|
881
|
+
// Also do an initial fetch for immediate data
|
|
882
|
+
fetchStatus()
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Cleanup on page unload
|
|
886
|
+
window.addEventListener('beforeunload', () => {
|
|
887
|
+
if (eventSource) eventSource.close()
|
|
888
|
+
if (reconnectTimeout) clearTimeout(reconnectTimeout)
|
|
889
|
+
stopPolling()
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
// Start
|
|
893
|
+
init()
|
|
894
|
+
</script>
|
|
895
|
+
</body>
|
|
896
|
+
</html>
|