@fugood/buttress-server 2.26.0-beta.2 → 2.26.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +415 -10
  2. package/config/function-samples/README.md +1 -0
  3. package/config/function-samples/classify-ticket.ts +73 -0
  4. package/config/function-samples/run-agent.ts +39 -0
  5. package/config/sample.toml +67 -0
  6. package/lib/agent/cli.d.ts +19 -0
  7. package/lib/agent/client.d.ts +66 -0
  8. package/lib/agent/codemode-worker.d.ts +1 -0
  9. package/lib/agent/codemode.d.ts +7 -0
  10. package/lib/agent/config.d.ts +15 -0
  11. package/lib/agent/context.d.ts +11 -0
  12. package/lib/agent/decisions.check.d.ts +1 -0
  13. package/lib/agent/decisions.d.ts +113 -0
  14. package/lib/agent/device-auth.d.ts +3 -0
  15. package/lib/agent/device-tools.d.ts +5 -0
  16. package/lib/agent/loopback.d.ts +21 -0
  17. package/lib/agent/mcp.d.ts +22 -0
  18. package/lib/agent/models.d.ts +22 -0
  19. package/lib/agent/service.d.ts +16 -0
  20. package/lib/agent/sessions.d.ts +30 -0
  21. package/lib/agent/tools.d.ts +35 -0
  22. package/lib/agent/tui.d.ts +17 -0
  23. package/lib/agent/types.d.ts +155 -0
  24. package/lib/cli-KbjOekMI.mjs +22 -0
  25. package/lib/client-qqI-HCOj.mjs +8 -0
  26. package/lib/codemode-Cva7OeQX.mjs +112 -0
  27. package/lib/codemode-worker.mjs +3 -0
  28. package/lib/command-nXCdWBlc.mjs +38 -0
  29. package/lib/config-DiYweJ2m.mjs +2 -0
  30. package/lib/functions/executor.d.ts +7 -1
  31. package/lib/functions/index.d.ts +3 -1
  32. package/lib/functions/status.d.ts +1 -1
  33. package/lib/functions/templates.d.ts +1 -1
  34. package/lib/functions/types.d.ts +15 -2
  35. package/lib/index.d.ts +12 -5
  36. package/lib/index.mjs +146 -93
  37. package/lib/mcp-DzknURyz.mjs +5 -0
  38. package/lib/mlx-bridge.py +707 -0
  39. package/lib/net-OvedjqrB.mjs +46 -0
  40. package/lib/protocol-WkPByalT.mjs +468 -0
  41. package/lib/quickjs.wasm +0 -0
  42. package/lib/rolldown-runtime-dTnj95Mm.mjs +2 -0
  43. package/lib/routes/agents.d.ts +66 -0
  44. package/lib/routes/decision-shared.d.ts +28 -0
  45. package/lib/routes/generator-cache.d.ts +3 -1
  46. package/lib/routes/index.d.ts +2 -0
  47. package/lib/routes/llm-shared.d.ts +8 -0
  48. package/lib/routes/openai-compat.d.ts +13 -2
  49. package/lib/routes/openai-decisions.check.d.ts +1 -0
  50. package/lib/routes/openai-decisions.d.ts +97 -0
  51. package/lib/routes/status.d.ts +2 -1
  52. package/lib/routes/typesafe-compat.check.d.ts +1 -0
  53. package/lib/routes/typesafe-compat.d.ts +30 -0
  54. package/lib/services/ggml-decision.d.ts +80 -0
  55. package/lib/services/ggml-llm.d.ts +29 -1
  56. package/lib/services/index.d.ts +3 -0
  57. package/lib/tui-ClNupsCH.mjs +2 -0
  58. package/lib/types.d.ts +48 -2
  59. package/lib/ui/command.d.ts +2 -0
  60. package/lib/ui/host.d.ts +1 -0
  61. package/lib/ui/window-module.d.ts +6 -0
  62. package/lib/utils/config.d.ts +3 -3
  63. package/lib/utils/cors.d.ts +1 -0
  64. package/lib/utils/decision-sharing.d.ts +37 -0
  65. package/lib/utils/downloadStatus.d.ts +57 -0
  66. package/lib/utils/hostFacts.d.ts +13 -0
  67. package/lib/utils/workspaceState.d.ts +8 -0
  68. package/lib/wrapper-3PX6qE3t.mjs +7 -0
  69. package/package.json +17 -9
  70. package/public/status.html +219 -1489
@@ -1,1494 +1,224 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
-
4
3
  <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Buttress Server Status</title>
8
- <style>
9
- :root {
10
- --bg: #1a1a2e;
11
- --bg-secondary: #16213e;
12
- --bg-card: #1f2940;
13
- --text: #e6e6e6;
14
- --text-muted: #8892a8;
15
- --accent: #4a90d9;
16
- --success: #4caf50;
17
- --warning: #ff9800;
18
- --error: #f44336;
19
- --border: #2d3a50;
20
- }
21
-
22
- * {
23
- box-sizing: border-box;
24
- margin: 0;
25
- padding: 0;
26
- }
27
-
28
- body {
29
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
30
- background: var(--bg);
31
- color: var(--text);
32
- line-height: 1.5;
33
- padding: 16px;
34
- min-height: 100vh;
35
- -webkit-text-size-adjust: 100%;
36
- }
37
-
38
- .container {
39
- max-width: 1400px;
40
- margin: 0 auto;
41
- overflow-x: hidden;
42
- }
43
-
44
- header {
45
- display: flex;
46
- flex-direction: column;
47
- gap: 12px;
48
- margin-bottom: 20px;
49
- padding-bottom: 16px;
50
- border-bottom: 1px solid var(--border);
51
- }
52
-
53
- .header-top {
54
- display: flex;
55
- justify-content: space-between;
56
- align-items: center;
57
- }
58
-
59
- .header-bottom {
60
- display: flex;
61
- justify-content: space-between;
62
- align-items: center;
63
- flex-wrap: wrap;
64
- gap: 8px;
65
- }
66
-
67
- h1 {
68
- font-size: 20px;
69
- font-weight: 600;
70
- }
71
-
72
- .connection-status {
73
- display: flex;
74
- align-items: center;
75
- gap: 6px;
76
- font-size: 13px;
77
- }
78
-
79
- .status-dot {
80
- width: 8px;
81
- height: 8px;
82
- border-radius: 50%;
83
- background: var(--text-muted);
84
- flex-shrink: 0;
85
- }
86
-
87
- .status-dot.connected {
88
- background: var(--success);
89
- }
90
-
91
- .status-dot.connecting {
92
- background: var(--warning);
93
- animation: pulse 1s infinite;
94
- }
95
-
96
- .status-dot.error {
97
- background: var(--error);
98
- }
99
-
100
- @keyframes pulse {
101
-
102
- 0%,
103
- 100% {
104
- opacity: 1;
105
- }
106
-
107
- 50% {
108
- opacity: 0.5;
109
- }
110
- }
111
-
112
- .grid {
113
- display: grid;
114
- grid-template-columns: 1fr;
115
- gap: 16px;
116
- max-width: 100%;
117
- }
118
-
119
- .card {
120
- background: var(--bg-card);
121
- border-radius: 8px;
122
- padding: 16px;
123
- border: 1px solid var(--border);
124
- overflow: hidden;
125
- min-width: 0;
126
- }
127
-
128
- .card-header {
129
- display: flex;
130
- justify-content: space-between;
131
- align-items: center;
132
- margin-bottom: 12px;
133
- }
134
-
135
- .card-title {
136
- font-size: 15px;
137
- font-weight: 600;
138
- color: var(--accent);
139
- }
140
-
141
- .badge {
142
- padding: 3px 6px;
143
- border-radius: 4px;
144
- font-size: 11px;
145
- font-weight: 500;
146
- white-space: nowrap;
147
- }
148
-
149
- .badge-success {
150
- background: rgba(76, 175, 80, 0.2);
151
- color: var(--success);
152
- }
153
-
154
- .badge-warning {
155
- background: rgba(255, 152, 0, 0.2);
156
- color: var(--warning);
157
- }
158
-
159
- .badge-error {
160
- background: rgba(244, 67, 54, 0.2);
161
- color: var(--error);
162
- }
163
-
164
- .badge-info {
165
- background: rgba(74, 144, 217, 0.2);
166
- color: var(--accent);
167
- }
168
-
169
- .table-wrapper {
170
- overflow-x: auto;
171
- -webkit-overflow-scrolling: touch;
172
- scrollbar-width: thin;
173
- scrollbar-color: var(--border) transparent;
174
- padding-bottom: 8px;
175
- }
176
-
177
- .table-wrapper::-webkit-scrollbar {
178
- height: 6px;
179
- }
180
-
181
- .table-wrapper::-webkit-scrollbar-track {
182
- background: transparent;
183
- }
184
-
185
- .table-wrapper::-webkit-scrollbar-thumb {
186
- background: var(--border);
187
- border-radius: 3px;
188
- }
189
-
190
- .table-inner {
191
- display: inline-block;
192
- min-width: 100%;
193
- }
194
-
195
- table {
196
- border-collapse: collapse;
197
- font-size: 12px;
198
- white-space: nowrap;
199
- }
200
-
201
- th,
202
- td {
203
- text-align: left;
204
- padding: 8px 10px;
205
- border-bottom: 1px solid var(--border);
206
- white-space: nowrap;
207
- }
208
-
209
- th {
210
- font-weight: 500;
211
- color: var(--text-muted);
212
- font-size: 11px;
213
- text-transform: uppercase;
214
- letter-spacing: 0.5px;
215
- position: sticky;
216
- top: 0;
217
- background: var(--bg-card);
218
- }
219
-
220
- tr:active td {
221
- background: rgba(255, 255, 255, 0.05);
222
- }
223
-
224
- .empty-state {
225
- text-align: center;
226
- padding: 24px 16px;
227
- color: var(--text-muted);
228
- font-size: 13px;
229
- }
230
-
231
- .section {
232
- margin-bottom: 16px;
233
- min-width: 0;
234
- }
235
-
236
- .section-title {
237
- font-size: 12px;
238
- font-weight: 600;
239
- color: var(--text-muted);
240
- margin-bottom: 10px;
241
- text-transform: uppercase;
242
- letter-spacing: 0.5px;
243
- padding: 8px 0;
244
- display: flex;
245
- align-items: center;
246
- -webkit-tap-highlight-color: transparent;
247
- }
248
-
249
- .collapsible {
250
- cursor: pointer;
251
- user-select: none;
252
- }
253
-
254
- .collapsible::before {
255
- content: '▼';
256
- display: inline-block;
257
- margin-right: 8px;
258
- font-size: 10px;
259
- transition: transform 0.2s;
260
- }
261
-
262
- .collapsible.collapsed::before {
263
- transform: rotate(-90deg);
264
- }
265
-
266
- .collapsible-content {
267
- overflow: hidden;
268
- transition: max-height 0.3s;
269
- }
270
-
271
- .collapsible-content.collapsed {
272
- max-height: 0 !important;
273
- }
274
-
275
- .stat-grid {
276
- display: grid;
277
- grid-template-columns: repeat(3, 1fr);
278
- gap: 8px;
279
- margin-bottom: 16px;
280
- }
281
-
282
- .stat-item {
283
- background: var(--bg-secondary);
284
- padding: 10px 8px;
285
- border-radius: 6px;
286
- text-align: center;
287
- }
288
-
289
- .stat-value {
290
- font-size: 20px;
291
- font-weight: 600;
292
- color: var(--accent);
293
- }
294
-
295
- .stat-label {
296
- font-size: 10px;
297
- color: var(--text-muted);
298
- text-transform: uppercase;
299
- margin-top: 2px;
300
- }
301
-
302
- .request-state {
303
- display: inline-block;
304
- padding: 2px 5px;
305
- border-radius: 3px;
306
- font-size: 10px;
307
- font-weight: 500;
308
- }
309
-
310
- .request-state.queued {
311
- background: rgba(74, 144, 217, 0.2);
312
- color: var(--accent);
313
- }
314
-
315
- .request-state.processing_prompt {
316
- background: rgba(255, 152, 0, 0.2);
317
- color: var(--warning);
318
- }
319
-
320
- .request-state.generating {
321
- background: rgba(76, 175, 80, 0.2);
322
- color: var(--success);
323
- }
324
-
325
- .request-state.done {
326
- background: rgba(128, 128, 128, 0.2);
327
- color: var(--text-muted);
328
- }
329
-
330
- .timestamp {
331
- color: var(--text-muted);
332
- font-size: 11px;
333
- }
334
-
335
- .mono {
336
- font-family: 'SF Mono', 'Menlo', monospace;
337
- font-size: 11px;
338
- }
339
-
340
- .model-name {
341
- font-family: 'SF Mono', 'Menlo', monospace;
342
- font-size: 10px;
343
- max-width: 120px;
344
- overflow: hidden;
345
- text-overflow: ellipsis;
346
- white-space: nowrap;
347
- display: inline-block;
348
- vertical-align: middle;
349
- }
350
-
351
- .refresh-btn {
352
- background: var(--accent);
353
- color: white;
354
- border: none;
355
- padding: 10px 16px;
356
- border-radius: 6px;
357
- cursor: pointer;
358
- font-size: 13px;
359
- font-weight: 500;
360
- -webkit-tap-highlight-color: transparent;
361
- touch-action: manipulation;
362
- }
363
-
364
- .refresh-btn:active {
365
- opacity: 0.8;
366
- transform: scale(0.98);
367
- }
368
-
369
- .last-update {
370
- font-size: 11px;
371
- color: var(--text-muted);
372
- }
373
-
374
- /* Tablet and up */
375
- @media (min-width: 768px) {
376
- body {
377
- padding: 20px;
378
- }
379
-
380
- header {
381
- flex-direction: row;
382
- justify-content: space-between;
383
- align-items: center;
384
- }
385
-
386
- .header-top {
387
- flex: 1;
388
- }
389
-
390
- .header-bottom {
391
- flex: none;
392
- gap: 16px;
393
- }
394
-
395
- h1 {
396
- font-size: 24px;
397
- }
398
-
399
- .grid {
400
- grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
401
- gap: 20px;
402
- }
403
-
404
- .card {
405
- padding: 20px;
406
- }
407
-
408
- table {
409
- font-size: 13px;
410
- }
411
-
412
- th,
413
- td {
414
- padding: 10px 12px;
415
- }
416
-
417
- .stat-value {
418
- font-size: 24px;
419
- }
420
-
421
- .stat-label {
422
- font-size: 11px;
423
- }
424
-
425
- .table-wrapper {
426
- margin: 0;
427
- padding: 0;
428
- }
429
- }
430
-
431
- /* Large screens */
432
- @media (min-width: 1200px) {
433
- .grid {
434
- grid-template-columns: repeat(2, 1fr);
435
- }
436
- }
437
- </style>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>Buttress Status</title>
8
+ <!-- Self-contained: the page is served by buttress-server on a LAN, often without internet. -->
9
+ <style>
10
+ :root {
11
+ color-scheme: light dark;
12
+ --bg: #F3F3F0; --panel: #FFFFFF; --panel-2: #F8F8F6; --line: #DCDCD6; --text: #1B1B19; --muted: #5D5D58;
13
+ --accent: #1F5FBF; --accent-soft: #E4ECFA; --ok: #1A7A3A; --ok-soft: #E0F2E5;
14
+ --warn: #945600; --warn-soft: #FBEFD6; --err: #B3261E; --err-soft: #FBE3E0;
15
+ --on-accent: #FFFFFF; --on-warn: #FFFFFF;
16
+ --sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
17
+ --mono: 'SF Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
18
+ }
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --bg: #121212; --panel: #1B1B1B; --panel-2: #212121; --line: #2E2E2E; --text: #ECECE8; --muted: #A0A09A;
22
+ --accent: #79A8F2; --accent-soft: #1A2A45; --ok: #62C684; --ok-soft: #14301E;
23
+ --warn: #E2AA4E; --warn-soft: #382B10; --err: #F27A70; --err-soft: #3B1916;
24
+ --on-accent: #0E1A2E; --on-warn: #1A1200;
25
+ }
26
+ }
27
+
28
+ * { box-sizing: border-box; }
29
+ /* Keeps focused or scrolled-to elements clear of the sticky header. */
30
+ html { scroll-padding-top: 72px; }
31
+ body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 var(--sans); -webkit-text-size-adjust: 100%; }
32
+ h1, h2, h3, p, ul, dl, dd, pre { margin: 0; }
33
+ ul { list-style: none; padding: 0; }
34
+ .mono { font-family: var(--mono); font-size: 0.93em; }
35
+ .help { font-size: 12px; color: var(--muted); line-height: 1.4; }
36
+ .t-ok { color: var(--ok); } .t-warn { color: var(--warn); } .t-err { color: var(--err); }
37
+ .t-muted { color: var(--muted); } .t-accent { color: var(--accent); } .t-neutral { color: var(--text); }
38
+
39
+ /* Tones: soft fill + strong text; muted and neutral are outlined chips. */
40
+ .tone-ok { background: var(--ok-soft); color: var(--ok); }
41
+ .tone-warn { background: var(--warn-soft); color: var(--warn); }
42
+ .tone-err { background: var(--err-soft); color: var(--err); }
43
+ .tone-accent { background: var(--accent-soft); color: var(--accent); }
44
+ .tone-muted { background: var(--panel-2); color: var(--muted); box-shadow: inset 0 0 0 1px var(--line); }
45
+ .tone-neutral { background: var(--panel-2); color: var(--text); box-shadow: inset 0 0 0 1px var(--line); }
46
+ .badge { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border-radius: 6px; font-size: 12px; font-weight: 500; white-space: nowrap; vertical-align: middle; }
47
+
48
+ /* Header */
49
+ .top { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 8px 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
50
+ .brand { display: flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0; }
51
+ .brand h1 { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
52
+ .pill { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; white-space: nowrap; min-height: 24px; padding: 2px 8px; border-radius: 6px; font-size: 12px; font-weight: 500; }
53
+ .pill .dot { width: 8px; height: 8px; border-radius: 4px; background: currentColor; flex-shrink: 0; }
54
+ .btn { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 44px; height: 44px; padding: 0 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--text); font: inherit; font-weight: 500; cursor: pointer; }
55
+ .btn:hover { background: var(--panel-2); }
56
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
57
+ @media (max-width: 599px) {
58
+ .btn-label, .pill-detail { display: none; }
59
+ .btn { padding: 0; }
60
+ }
61
+
62
+ /* Page */
63
+ main { max-width: 1440px; margin: 0 auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
64
+ .empty-page { padding: 24px 0; text-align: center; color: var(--muted); }
65
+ .foot { text-align: center; margin-top: 4px; }
66
+
67
+ .banner { display: flex; align-items: flex-start; gap: 10px 12px; flex-wrap: wrap; padding: 10px 14px; border-radius: 8px; font-size: 13px; }
68
+ .banner-icon { flex-shrink: 0; margin-top: 1px; }
69
+ .banner-text { flex: 1 1 280px; min-width: 0; }
70
+ .banner-cmd { font: 12px var(--mono); background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; overflow-wrap: anywhere; }
71
+
72
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; min-width: 0; overflow: hidden; }
73
+ .card-head { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); }
74
+ .card-head h2, .block-head h2 { font-size: 14px; font-weight: 600; }
75
+ .block-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
76
+
77
+ /* Identity */
78
+ .identity { display: flex; flex-wrap: wrap; gap: 12px 24px; padding: 14px; }
79
+ .identity-main { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
80
+ .identity-title { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
81
+ .identity-title h2 { font-size: 16px; font-weight: 600; margin-right: 2px; }
82
+ .identity-id { overflow-wrap: anywhere; }
83
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
84
+ .chip { font: 12px var(--mono); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; overflow-wrap: anywhere; }
85
+ .facts { flex: 1 1 420px; min-width: 0; display: flex; flex-direction: column; }
86
+ .fact { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
87
+ .fact:last-child { border-bottom: 0; }
88
+ .fact dt { color: var(--muted); }
89
+ .fact dd { text-align: right; min-width: 0; overflow-wrap: anywhere; }
90
+ /* A long on/off value ("JWT required on data paths") wraps instead of spilling into the next fact. */
91
+ .fact .badge { white-space: normal; height: auto; min-height: 20px; padding-top: 1px; padding-bottom: 1px; }
92
+ /* An endpoint's path after its on badge; phones keep the badge only. */
93
+ .fact-detail { margin-left: 4px; }
94
+ @media (max-width: 599px) { .fact-detail { display: none; } }
95
+
96
+ /* Hardware */
97
+ .hardware { display: flex; flex-direction: column; gap: 6px; }
98
+ .caps { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
99
+ .cap { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
100
+ .cap-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
101
+ .cap-head strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
102
+ @media (max-width: 599px) { .score-word { display: none; } }
103
+
104
+ /* Downloads */
105
+ .dl { display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; font-size: 13px; }
106
+ .dl + .dl { border-top: 1px solid var(--line); }
107
+ .dl-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; min-width: 0; }
108
+ .dl-title { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
+ .bar { height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; }
110
+ .bar > span { display: block; height: 100%; background: var(--accent); }
111
+ .bar.indeterminate > span { width: 30%; animation: indeterminate 1.4s ease-in-out infinite; }
112
+ @keyframes indeterminate { from { transform: translateX(-100%); } to { transform: translateX(340%); } }
113
+ @media (prefers-reduced-motion: reduce) { .bar.indeterminate > span { animation: none; width: 100%; opacity: 0.4; } }
114
+
115
+ /* Not running */
116
+ .not-running { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
117
+ .nr-head { display: flex; align-items: center; gap: 14px; }
118
+ .nr-head h2 { font-size: 16px; font-weight: 600; }
119
+ .nr-icon { width: 40px; height: 40px; flex-shrink: 0; border-radius: 20px; background: var(--panel-2); border: 1px solid var(--line); color: var(--muted); display: inline-flex; align-items: center; justify-content: center; }
120
+ .nr-facts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px 24px; font-size: 13px; }
121
+ .nr-facts dt { color: var(--muted); }
122
+ .nr-facts dd { overflow-wrap: anywhere; }
123
+ .nr-start { display: flex; flex-direction: column; gap: 8px; }
124
+ .nr-label { font-size: 13px; font-weight: 500; }
125
+ .cmd-block { padding: 10px 14px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; font: 13px var(--mono); overflow-x: auto; }
126
+
127
+ /* Sections */
128
+ .sections { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
129
+ .sec-head { font-size: 14px; }
130
+ .card-toggle { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 48px; padding: 10px 14px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
131
+ .card-toggle[aria-expanded="true"] { border-bottom: 1px solid var(--line); }
132
+ .card-toggle:hover { background: var(--panel-2); }
133
+ .sec-title { font-weight: 600; white-space: nowrap; }
134
+ .sec-sub { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
135
+ .chev { flex-shrink: 0; color: var(--muted); transition: transform 0.15s ease; }
136
+ .card-toggle[aria-expanded="false"] .chev { transform: rotate(-90deg); }
137
+ @media (prefers-reduced-motion: reduce) { .chev { transition: none; } }
138
+
139
+ .sec-body > .block { border-top: 1px solid var(--line); }
140
+ /* Histories sit side by side only when each table fits whole (the basis is the table's own
141
+ width), else each takes the full row. The shadow draws the divider above and to the left of
142
+ each one whichever way they wrap (the card clips the part on its outer edge). */
143
+ .split { display: flex; flex-wrap: wrap; }
144
+ .split > .block { flex: 1 1 auto; min-width: 0; box-shadow: -1px -1px 0 var(--line); }
145
+ .block-title { padding: 10px 14px 4px; font-size: 12px; font-weight: 500; color: var(--muted); }
146
+ .stats { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--line); }
147
+ .stat { display: flex; flex-direction: column; gap: 2px; flex: 1 1 72px; min-width: 72px; padding: 8px 10px; background: var(--panel-2); border-radius: 8px; }
148
+ .stat b { font: 600 18px/1.2 var(--mono); }
149
+ .stat span { font-size: 11px; color: var(--muted); }
150
+ .empty { padding: 10px 14px 12px; font-size: 13px; color: var(--muted); }
151
+
152
+ /* Tables swipe sideways inside their box instead of widening the page. */
153
+ .table-box { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
154
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
155
+ th { text-align: left; font-weight: 500; color: var(--muted); padding: 8px 12px; border-bottom: 1px solid var(--line); white-space: nowrap; }
156
+ th.mono { font-family: inherit; font-size: inherit; }
157
+ th.grow { min-width: 160px; }
158
+ td { padding: 8px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; white-space: nowrap; }
159
+ tbody tr:last-child td { border-bottom: 0; }
160
+ td .primary { font-weight: 500; }
161
+ .cell-sub { display: block; max-width: 280px; font: 11px/1.35 var(--mono); color: var(--muted); overflow: hidden; text-overflow: ellipsis; }
162
+
163
+ @media (min-width: 900px) {
164
+ .top { padding: 0 24px; min-height: 52px; }
165
+ .btn { height: 32px; }
166
+ main { padding: 20px 24px 32px; gap: 16px; }
167
+ .identity { padding: 16px 20px; }
168
+ .identity-title h2 { font-size: 18px; }
169
+ .facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 20px; align-content: start; }
170
+ .fact { flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 2px; padding: 0; border: 0; }
171
+ .fact dt { font-size: 12px; }
172
+ .fact dd { text-align: left; }
173
+ .caps { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
174
+ .nr-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
175
+ .sections { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
176
+ .sec.wide { grid-column: 1 / -1; }
177
+ .stat { flex: 0 0 auto; min-width: 96px; }
178
+ }
179
+ </style>
438
180
  </head>
439
-
440
181
  <body>
441
- <div class="container">
442
- <header>
443
- <div class="header-top">
444
- <h1>Buttress Status</h1>
445
- <div class="connection-status">
446
- <span class="status-dot" id="connectionDot"></span>
447
- <span id="connectionText">Connecting...</span>
448
- </div>
449
- </div>
450
- <div class="header-bottom">
451
- <span class="last-update" id="lastUpdate">-</span>
452
- <button class="refresh-btn" onclick="refreshStatus()">Refresh</button>
453
- </div>
454
- </header>
455
-
456
- <div class="grid">
457
- <!-- LLM Status -->
458
- <div class="card">
459
- <div class="card-header">
460
- <span class="card-title">GGML-LLM Status</span>
461
- <span class="badge badge-info" id="llmGeneratorCount">0 generators</span>
462
- </div>
463
-
464
- <div id="llmGenerators"></div>
465
-
466
- <div class="section">
467
- <div class="section-title collapsible" onclick="toggleSection(this)">Parallel Status</div>
468
- <div class="collapsible-content" id="llmParallelStatus">
469
- <div class="empty-state">No active parallel requests</div>
470
- </div>
471
- </div>
472
-
473
- <div class="section">
474
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
475
- <div class="collapsible-content" id="llmModelHistory">
476
- <div class="empty-state">No model load history</div>
477
- </div>
478
- </div>
479
-
480
- <div class="section">
481
- <div class="section-title collapsible" onclick="toggleSection(this)">Completion History</div>
482
- <div class="collapsible-content" id="llmCompletionHistory">
483
- <div class="empty-state">No completion history</div>
484
- </div>
485
- </div>
486
- </div>
487
-
488
- <!-- STT Status -->
489
- <div class="card">
490
- <div class="card-header">
491
- <span class="card-title">GGML-STT Status</span>
492
- <span class="badge badge-info" id="sttGeneratorCount">0 generators</span>
493
- </div>
494
-
495
- <div id="sttGenerators"></div>
496
-
497
- <div class="section">
498
- <div class="section-title collapsible" onclick="toggleSection(this)">Queue Status</div>
499
- <div class="collapsible-content" id="sttQueueStatus">
500
- <div class="empty-state">No queue info</div>
501
- </div>
502
- </div>
503
-
504
- <div class="section">
505
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
506
- <div class="collapsible-content" id="sttModelHistory">
507
- <div class="empty-state">No model load history</div>
508
- </div>
509
- </div>
510
-
511
- <div class="section">
512
- <div class="section-title collapsible" onclick="toggleSection(this)">Transcription History</div>
513
- <div class="collapsible-content" id="sttTranscriptionHistory">
514
- <div class="empty-state">No transcription history</div>
515
- </div>
516
- </div>
517
- </div>
518
-
519
- <!-- MLX-LLM Status (hidden unless server has MLX generators) -->
520
- <div class="card" id="mlxCard" style="display:none">
521
- <div class="card-header">
522
- <span class="card-title">MLX-LLM Status</span>
523
- <span class="badge badge-info" id="mlxGeneratorCount">0 generators</span>
524
- </div>
525
-
526
- <div id="mlxGenerators"></div>
527
-
528
- <div class="section">
529
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
530
- <div class="collapsible-content" id="mlxModelHistory">
531
- <div class="empty-state">No model load history</div>
532
- </div>
533
- </div>
534
-
535
- <div class="section">
536
- <div class="section-title collapsible" onclick="toggleSection(this)">Completion History</div>
537
- <div class="collapsible-content" id="mlxCompletionHistory">
538
- <div class="empty-state">No completion history</div>
539
- </div>
540
- </div>
541
- </div>
542
-
543
- <!-- ONNX-STT Status (hidden unless active) -->
544
- <div class="card" id="onnxSttCard" style="display:none">
545
- <div class="card-header">
546
- <span class="card-title">ONNX-STT Status</span>
547
- <span class="badge badge-info" id="onnxSttGeneratorCount">0 generators</span>
548
- </div>
549
- <div id="onnxSttGenerators"></div>
550
- <div class="section">
551
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
552
- <div class="collapsible-content" id="onnxSttModelHistory">
553
- <div class="empty-state">No model load history</div>
554
- </div>
555
- </div>
556
- <div class="section">
557
- <div class="section-title collapsible" onclick="toggleSection(this)">Transcription History</div>
558
- <div class="collapsible-content" id="onnxSttTranscriptionHistory">
559
- <div class="empty-state">No transcription history</div>
560
- </div>
561
- </div>
562
- </div>
563
-
564
- <!-- ONNX-TTS Status (hidden unless active) -->
565
- <div class="card" id="onnxTtsCard" style="display:none">
566
- <div class="card-header">
567
- <span class="card-title">ONNX-TTS Status</span>
568
- <span class="badge badge-info" id="onnxTtsGeneratorCount">0 generators</span>
569
- </div>
570
- <div id="onnxTtsGenerators"></div>
571
- <div class="section">
572
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
573
- <div class="collapsible-content" id="onnxTtsModelHistory">
574
- <div class="empty-state">No model load history</div>
575
- </div>
576
- </div>
577
- <div class="section">
578
- <div class="section-title collapsible" onclick="toggleSection(this)">Synthesis History</div>
579
- <div class="collapsible-content" id="onnxTtsSynthesisHistory">
580
- <div class="empty-state">No synthesis history</div>
581
- </div>
582
- </div>
583
- </div>
584
-
585
- <!-- GGML-TTS Status (hidden unless active) -->
586
- <div class="card" id="ggmlTtsCard" style="display:none">
587
- <div class="card-header">
588
- <span class="card-title">GGML-TTS Status</span>
589
- <span class="badge badge-info" id="ggmlTtsGeneratorCount">0 generators</span>
590
- </div>
591
- <div id="ggmlTtsGenerators"></div>
592
- <div class="section">
593
- <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
594
- <div class="collapsible-content" id="ggmlTtsModelHistory">
595
- <div class="empty-state">No model load history</div>
596
- </div>
597
- </div>
598
- <div class="section">
599
- <div class="section-title collapsible" onclick="toggleSection(this)">Synthesis History</div>
600
- <div class="collapsible-content" id="ggmlTtsSynthesisHistory">
601
- <div class="empty-state">No synthesis history</div>
602
- </div>
603
- </div>
604
- </div>
605
-
606
- <!-- Local Functions (hidden unless enabled) -->
607
- <div class="card" id="functionsCard" style="display:none">
608
- <div class="card-header">
609
- <span class="card-title">Local Functions <span class="badge badge-warning">experimental</span></span>
610
- <span class="badge badge-info" id="functionsCount">0 functions</span>
611
- </div>
612
- <div id="functionsSummary"></div>
613
- <div class="section" id="functionsDaemonsSection" style="display:none">
614
- <div class="section-title collapsible" onclick="toggleSection(this)">Daemons</div>
615
- <div class="collapsible-content" id="functionsDaemons"></div>
616
- </div>
617
- <div class="section" id="functionsDaemonHistorySection" style="display:none">
618
- <div class="section-title collapsible" onclick="toggleSection(this)">Daemon Activity</div>
619
- <div class="collapsible-content" id="functionsDaemonHistory">
620
- <div class="empty-state">No daemon activity</div>
621
- </div>
622
- </div>
623
- <div class="section">
624
- <div class="section-title collapsible" onclick="toggleSection(this)">Call History</div>
625
- <div class="collapsible-content" id="functionsCallHistory">
626
- <div class="empty-state">No function calls</div>
627
- </div>
628
- </div>
629
- <div class="section">
630
- <div class="section-title collapsible" onclick="toggleSection(this)">Upload History</div>
631
- <div class="collapsible-content" id="functionsUploadHistory">
632
- <div class="empty-state">No uploads</div>
633
- </div>
634
- </div>
635
- <div class="section">
636
- <div class="section-title collapsible" onclick="toggleSection(this)">Download History</div>
637
- <div class="collapsible-content" id="functionsDownloadHistory">
638
- <div class="empty-state">No downloads</div>
639
- </div>
640
- </div>
641
- <div class="section">
642
- <div class="section-title collapsible" onclick="toggleSection(this)">Auth History</div>
643
- <div class="collapsible-content" id="functionsAuthHistory">
644
- <div class="empty-state">No auth activity</div>
645
- </div>
646
- </div>
647
- </div>
648
- </div>
649
- </div>
650
-
651
- <script>
652
- // Relative time formatting
653
- function formatRelativeTime(timestamp) {
654
- const now = Date.now()
655
- const then = new Date(timestamp).getTime()
656
- const diff = now - then
657
-
658
- if (diff < 1000) return 'just now'
659
- if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`
660
- if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
661
- if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
662
- return new Date(timestamp).toLocaleString()
663
- }
664
-
665
- // Toggle collapsible sections
666
- function toggleSection(el) {
667
- el.classList.toggle('collapsed')
668
- el.nextElementSibling.classList.toggle('collapsed')
669
- }
670
-
671
- // Update connection status
672
- function setConnectionStatus(status) {
673
- const dot = document.getElementById('connectionDot')
674
- const text = document.getElementById('connectionText')
675
- dot.className = 'status-dot ' + status
676
- text.textContent = status === 'connected' ? 'Subscribed (Live)' :
677
- status === 'connecting' ? 'Connecting...' : 'Disconnected'
678
- }
679
-
680
- // Helper to preserve scroll position during re-render
681
- function withScrollPreserve(container, renderFn) {
682
- const wrapper = container.querySelector('.table-wrapper')
683
- const scrollLeft = wrapper?.scrollLeft || 0
684
- const scrollTop = wrapper?.scrollTop || 0
685
-
686
- renderFn()
687
-
688
- const newWrapper = container.querySelector('.table-wrapper')
689
- if (newWrapper) {
690
- newWrapper.scrollLeft = scrollLeft
691
- newWrapper.scrollTop = scrollTop
692
- }
693
- }
694
-
695
- // Render LLM generators
696
- function renderLlmGenerators(generators) {
697
- const container = document.getElementById('llmGenerators')
698
- document.getElementById('llmGeneratorCount').textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
699
-
700
- withScrollPreserve(container, () => {
701
- if (generators.length === 0) {
702
- container.innerHTML = '<div class="empty-state">No LLM generators loaded</div>'
703
- return
704
- }
705
-
706
- container.innerHTML = `
707
- <div class="table-wrapper">
708
- <div class="table-inner">
709
- <table>
710
- <thead>
711
- <tr>
712
- <th>ID</th>
713
- <th>Model</th>
714
- <th>Quant</th>
715
- <th>Variant</th>
716
- <th>Context</th>
717
- <th>Parallel</th>
718
- <th>Status</th>
719
- </tr>
720
- </thead>
721
- <tbody>
722
- ${generators.map(g => {
723
- const hasActiveContext = g.contexts?.some(c => c.hasModel) || false
724
- return `
725
- <tr>
726
- <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
727
- <td>${formatModelName(g.repoId)}</td>
728
- <td>${escapeHtml(g.quantization || '-')}</td>
729
- <td>${escapeHtml(g.variant || '-')}</td>
730
- <td>${g.nCtx?.toLocaleString() || '-'}</td>
731
- <td>${g.nParallel || '-'}</td>
732
- <td>${hasActiveContext ? '<span class="badge badge-success">Active</span>' : '<span class="badge badge-warning">Inactive</span>'}</td>
733
- </tr>
734
- `}).join('')}
735
- </tbody>
736
- </table>
737
- </div>
738
- </div>
739
- `
740
- })
741
- }
742
-
743
- // Render parallel statuses
744
- function renderParallelStatus(statuses) {
745
- const container = document.getElementById('llmParallelStatus')
746
-
747
- withScrollPreserve(container, () => {
748
- if (!statuses || statuses.length === 0) {
749
- container.innerHTML = '<div class="empty-state">No active parallel requests</div>'
750
- return
751
- }
752
-
753
- let html = ''
754
- for (const status of statuses) {
755
- html += `
756
- <div class="stat-grid">
757
- <div class="stat-item">
758
- <div class="stat-value">${status.n_parallel || 0}</div>
759
- <div class="stat-label">Parallel Slots</div>
760
- </div>
761
- <div class="stat-item">
762
- <div class="stat-value">${status.active_slots || 0}</div>
763
- <div class="stat-label">Active</div>
764
- </div>
765
- <div class="stat-item">
766
- <div class="stat-value">${status.queued_requests || 0}</div>
767
- <div class="stat-label">Queued</div>
768
- </div>
769
- </div>
770
- `
771
-
772
- if (status.requests && status.requests.length > 0) {
773
- html += `
774
- <div class="table-wrapper">
775
- <div class="table-inner">
776
- <table>
777
- <thead>
778
- <tr>
779
- <th>Request ID</th>
780
- <th>Type</th>
781
- <th>State</th>
782
- <th>Tokens</th>
783
- <th>Speed</th>
784
- </tr>
785
- </thead>
786
- <tbody>
787
- ${status.requests.map(r => `
788
- <tr>
789
- <td class="mono">${r.request_id}</td>
790
- <td>${r.type}</td>
791
- <td><span class="request-state ${r.state}">${r.state}</span></td>
792
- <td>${r.tokens_generated || 0}</td>
793
- <td>${r.tokens_per_second?.toFixed(1) || '-'} t/s</td>
794
- </tr>
795
- `).join('')}
796
- </tbody>
797
- </table>
798
- </div>
799
- </div>
800
- `
801
- }
802
- }
803
-
804
- container.innerHTML = html || '<div class="empty-state">No parallel status available</div>'
805
- })
806
- }
807
-
808
- // Render history tables (preserves scroll position)
809
- function renderHistory(containerId, items, columns) {
810
- const container = document.getElementById(containerId)
811
-
812
- withScrollPreserve(container, () => {
813
- if (!items || items.length === 0) {
814
- container.innerHTML = `<div class="empty-state">No history</div>`
815
- return
816
- }
817
-
818
- container.innerHTML = `
819
- <div class="table-wrapper">
820
- <div class="table-inner">
821
- <table>
822
- <thead>
823
- <tr>
824
- ${columns.map(c => `<th>${c.label}</th>`).join('')}
825
- </tr>
826
- </thead>
827
- <tbody>
828
- ${items.slice(0, 20).map(item => `
829
- <tr>
830
- ${columns.map(c => `<td>${c.render(item)}</td>`).join('')}
831
- </tr>
832
- `).join('')}
833
- </tbody>
834
- </table>
835
- </div>
836
- </div>
837
- `
838
- })
839
- }
840
-
841
- // Render STT generators
842
- function renderSttGenerators(generators) {
843
- const container = document.getElementById('sttGenerators')
844
- document.getElementById('sttGeneratorCount').textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
845
-
846
- withScrollPreserve(container, () => {
847
- if (generators.length === 0) {
848
- container.innerHTML = '<div class="empty-state">No STT generators loaded</div>'
849
- return
850
- }
851
-
852
- container.innerHTML = `
853
- <div class="table-wrapper">
854
- <div class="table-inner">
855
- <table>
856
- <thead>
857
- <tr>
858
- <th>ID</th>
859
- <th>Model</th>
860
- <th>Type</th>
861
- <th>Quant</th>
862
- <th>Variant</th>
863
- <th>Context</th>
864
- </tr>
865
- </thead>
866
- <tbody>
867
- ${generators.map(g => `
868
- <tr>
869
- <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
870
- <td>${formatModelName(g.repoId)}</td>
871
- <td>${escapeHtml(g.modelType || '-')}</td>
872
- <td>${escapeHtml(g.quantization || '-')}</td>
873
- <td>${escapeHtml(g.variant || '-')}</td>
874
- <td>${g.hasContext ? '<span class="badge badge-success">Active</span>' : '<span class="badge badge-warning">Inactive</span>'}</td>
875
- </tr>
876
- `).join('')}
877
- </tbody>
878
- </table>
879
- </div>
880
- </div>
881
- `
882
- })
883
- }
884
-
885
- // Render STT queue status
886
- function renderSttQueueStatus(generators) {
887
- const container = document.getElementById('sttQueueStatus')
888
- const queues = generators.filter(g => g.queueStatus)
889
-
890
- if (queues.length === 0) {
891
- container.innerHTML = '<div class="empty-state">No queue info available</div>'
892
- return
893
- }
894
-
895
- container.innerHTML = `
896
- <div class="stat-grid">
897
- ${queues.map(g => `
898
- <div class="stat-item">
899
- <div class="stat-value">${g.queueStatus.processing ? '1' : '0'}</div>
900
- <div class="stat-label">Processing</div>
901
- </div>
902
- <div class="stat-item">
903
- <div class="stat-value">${g.queueStatus.queuedCount || 0}</div>
904
- <div class="stat-label">Queued</div>
905
- </div>
906
- `).join('')}
907
- </div>
908
- `
909
- }
910
-
911
- // Render MLX-LLM generators
912
- function renderMlxGenerators(generators) {
913
- const container = document.getElementById('mlxGenerators')
914
- document.getElementById('mlxGeneratorCount').textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
915
-
916
- withScrollPreserve(container, () => {
917
- if (generators.length === 0) {
918
- container.innerHTML = '<div class="empty-state">No MLX generators loaded</div>'
919
- return
920
- }
921
-
922
- container.innerHTML = `
923
- <div class="table-wrapper">
924
- <div class="table-inner">
925
- <table>
926
- <thead>
927
- <tr>
928
- <th>ID</th>
929
- <th>Model</th>
930
- <th>Variant</th>
931
- <th>Status</th>
932
- </tr>
933
- </thead>
934
- <tbody>
935
- ${generators.map(g => {
936
- const hasActiveContext = g.contexts?.some(c => c.hasModel) || false
937
- return `
938
- <tr>
939
- <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
940
- <td>${formatModelName(g.repoId)}</td>
941
- <td>${escapeHtml(g.variant || 'mlx')}</td>
942
- <td>${hasActiveContext ? '<span class="badge badge-success">Active</span>' : '<span class="badge badge-warning">Inactive</span>'}</td>
943
- </tr>
944
- `}).join('')}
945
- </tbody>
946
- </table>
947
- </div>
948
- </div>
949
- `
950
- })
951
- }
952
-
953
- function formatBytes(bytes) {
954
- if (!bytes || bytes === 0) return '-'
955
- if (bytes < 1024) return bytes + ' B'
956
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
957
- if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
958
- return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
959
- }
960
-
961
- function renderOnnxGenerators(containerId, countId, generators, label) {
962
- const container = document.getElementById(containerId)
963
- document.getElementById(countId).textContent = `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
964
-
965
- withScrollPreserve(container, () => {
966
- if (generators.length === 0) {
967
- container.innerHTML = `<div class="empty-state">No ${label} generators loaded</div>`
968
- return
969
- }
970
-
971
- container.innerHTML = `
972
- <div class="table-wrapper">
973
- <div class="table-inner">
974
- <table>
975
- <thead>
976
- <tr>
977
- <th>ID</th>
978
- <th>Model</th>
979
- <th>DType</th>
980
- <th>Provider</th>
981
- <th>Device</th>
982
- <th>Model Size</th>
983
- <th>Pipelines</th>
984
- </tr>
985
- </thead>
986
- <tbody>
987
- ${generators.map(g => `
988
- <tr>
989
- <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
990
- <td>${formatModelName(g.repoId)}${g.vocoderRepoId ? '<br><span style="font-size:11px;color:var(--text-muted)">+ ' + formatModelName(g.vocoderRepoId) + '</span>' : ''}</td>
991
- <td>${escapeHtml(typeof g.dtype === 'object' ? JSON.stringify(g.dtype) : g.dtype || '-')}</td>
992
- <td><span class="badge ${g.provider === 'cpu' ? 'badge-info' : 'badge-success'}">${escapeHtml(g.provider || '-')}</span></td>
993
- <td>${escapeHtml(g.device || '-')}</td>
994
- <td>${formatBytes(g.modelBytes)}</td>
995
- <td>${g.pipelines ?? 0}</td>
996
- </tr>
997
- `).join('')}
998
- </tbody>
999
- </table>
1000
- </div>
1001
- </div>
1002
- `
1003
- })
1004
- }
1005
-
1006
- function renderGgmlTtsGenerators(generators) {
1007
- const container = document.getElementById('ggmlTtsGenerators')
1008
- document.getElementById('ggmlTtsGeneratorCount').textContent =
1009
- `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
1010
-
1011
- withScrollPreserve(container, () => {
1012
- if (generators.length === 0) {
1013
- container.innerHTML = '<div class="empty-state">No GGML TTS generators loaded</div>'
1014
- return
1015
- }
1016
-
1017
- container.innerHTML = `
1018
- <div class="table-wrapper">
1019
- <div class="table-inner">
1020
- <table>
1021
- <thead>
1022
- <tr>
1023
- <th>ID</th>
1024
- <th>Model</th>
1025
- <th>Quant</th>
1026
- <th>Variant</th>
1027
- <th>GPU</th>
1028
- <th>Model Size</th>
1029
- <th>Context</th>
1030
- </tr>
1031
- </thead>
1032
- <tbody>
1033
- ${generators.map(g => `
1034
- <tr>
1035
- <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
1036
- <td>${formatModelName(g.repoId)}${g.vocoderRepoId ? '<br><span style="font-size:11px;color:var(--text-muted)">+ ' + formatModelName(g.vocoderRepoId) + '</span>' : ''}</td>
1037
- <td>${escapeHtml(g.quantization || '-')}</td>
1038
- <td>${escapeHtml(g.variant || '-')}</td>
1039
- <td><span class="badge ${g.useGpu ? 'badge-success' : 'badge-info'}">${g.useGpu ? 'yes' : 'no'}</span></td>
1040
- <td>${formatBytes(g.modelBytes)}</td>
1041
- <td>${g.hasContext ? '<span class="badge badge-success">loaded</span>' : '-'}</td>
1042
- </tr>
1043
- `).join('')}
1044
- </tbody>
1045
- </table>
1046
- </div>
1047
- </div>
1048
- `
1049
- })
1050
- }
1051
-
1052
- // Escape HTML
1053
- function escapeHtml(str) {
1054
- if (str == null) return ''
1055
- return String(str)
1056
- .replace(/&/g, '&amp;')
1057
- .replace(/</g, '&lt;')
1058
- .replace(/>/g, '&gt;')
1059
- .replace(/"/g, '&quot;')
1060
- }
1061
-
1062
- // Format model name: remove owner, wrap in styled span with title for full name
1063
- function formatModelName(repoId) {
1064
- if (!repoId) return '-'
1065
- const name = repoId.includes('/') ? repoId.split('/').pop() : repoId
1066
- return `<span class="model-name" title="${escapeHtml(repoId)}">${escapeHtml(name)}</span>`
1067
- }
1068
-
1069
- // Update the UI with status data
1070
- function updateUI(status) {
1071
- document.getElementById('lastUpdate').textContent =
1072
- 'Updated ' + formatRelativeTime(status.timestamp || new Date().toISOString())
1073
-
1074
- const llm = status.ggmlLlm || {}
1075
- const stt = status.ggmlStt || {}
1076
- const mlx = status.mlxLlm || {}
1077
- const onnxStt = status.onnxStt || {}
1078
- const onnxTts = status.onnxTts || {}
1079
- const ggmlTts = status.ggmlTts || {}
1080
-
1081
- // LLM
1082
- renderLlmGenerators(llm.generators || [])
1083
- renderParallelStatus(llm.parallelStatuses || [])
1084
-
1085
- renderHistory('llmModelHistory', llm.history?.modelLoads || [], [
1086
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1087
- { label: 'Model', render: i => formatModelName(i.repoId) },
1088
- { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1089
- { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1090
- { label: 'Context', render: i => i.nCtx?.toLocaleString() || '-' },
1091
- { label: 'GPU Layers', render: i => i.nGpuLayers ?? '-' },
1092
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1093
- {
1094
- label: 'Status', render: i => i.success ?
1095
- '<span class="badge badge-success">Success</span>' :
1096
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1097
- },
1098
- ])
1099
-
1100
- renderHistory('llmCompletionHistory', llm.history?.completions || [], [
1101
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1102
- { label: 'Model', render: i => formatModelName(i.repoId) },
1103
- { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1104
- { label: 'Total', render: i => `${(i.cacheTokens || 0) + (i.promptTokens || 0) + (i.tokensGenerated || 0)}` },
1105
- { label: 'Cache', render: i => `${i.cacheTokens || 0}` },
1106
- { label: 'Prompt', render: i => `${i.promptTokens || 0}` },
1107
- { label: 'Generated', render: i => `${i.tokensGenerated || 0}` },
1108
- { label: 'Gen Speed', render: i => `${i.tokensPerSecond?.toFixed(1) || '-'} t/s` },
1109
- { label: 'Prompt Speed', render: i => `${i.promptPerSecond?.toFixed(1) || '-'} t/s` },
1110
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1111
- {
1112
- label: 'Status', render: i => {
1113
- if (!i.success) return '<span class="badge badge-error">Failed</span>'
1114
- if (i.interrupted) return '<span class="badge badge-warning">Interrupted</span>'
1115
- if (i.contextFull) return '<span class="badge badge-warning">Ctx Full</span>'
1116
- return '<span class="badge badge-success">Success</span>'
1117
- }
1118
- },
1119
- ])
1120
-
1121
- // STT
1122
- renderSttGenerators(stt.generators || [])
1123
- renderSttQueueStatus(stt.generators || [])
1124
-
1125
- renderHistory('sttModelHistory', stt.history?.modelLoads || [], [
1126
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1127
- { label: 'Model', render: i => formatModelName(i.repoId) },
1128
- { label: 'Type', render: i => escapeHtml(i.modelType || '-') },
1129
- { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1130
- { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1131
- { label: 'GPU', render: i => i.useGpu ? '<span class="badge badge-success">Yes</span>' : '<span class="badge badge-info">No</span>' },
1132
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1133
- {
1134
- label: 'Status', render: i => i.success ?
1135
- '<span class="badge badge-success">Success</span>' :
1136
- `<span class="badge badge-error">Failed</span>`
1137
- },
1138
- ])
1139
-
1140
- renderHistory('sttTranscriptionHistory', stt.history?.transcriptions || [], [
1141
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1142
- { label: 'Model', render: i => formatModelName(i.repoId) },
1143
- { label: 'Type', render: i => escapeHtml(i.modelType || '-') },
1144
- { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1145
- { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1146
- { label: 'Segments', render: i => i.segmentCount ?? '-' },
1147
- { label: 'Text Len', render: i => i.textLength ?? '-' },
1148
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1149
- {
1150
- label: 'Status', render: i => i.success ?
1151
- '<span class="badge badge-success">Success</span>' :
1152
- `<span class="badge badge-error">Failed</span>`
1153
- },
1154
- ])
1155
-
1156
- // MLX-LLM — only show card when server has MLX generators or history
1157
- const hasMlx = (mlx.generators?.length > 0) ||
1158
- (mlx.history?.modelLoads?.length > 0) ||
1159
- (mlx.history?.completions?.length > 0)
1160
- document.getElementById('mlxCard').style.display = hasMlx ? '' : 'none'
1161
-
1162
- renderMlxGenerators(mlx.generators || [])
1163
-
1164
- renderHistory('mlxModelHistory', mlx.history?.modelLoads || [], [
1165
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1166
- { label: 'Model', render: i => formatModelName(i.repoId) },
1167
- { label: 'Variant', render: i => escapeHtml(i.variant || 'mlx') },
1168
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1169
- {
1170
- label: 'Status', render: i => i.success ?
1171
- '<span class="badge badge-success">Success</span>' :
1172
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1173
- },
1174
- ])
1175
-
1176
- renderHistory('mlxCompletionHistory', mlx.history?.completions || [], [
1177
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1178
- { label: 'Model', render: i => formatModelName(i.repoId) },
1179
- { label: 'Total', render: i => `${(i.cacheTokens || 0) + (i.promptTokens || 0) + (i.tokensGenerated || 0)}` },
1180
- { label: 'Cache', render: i => `${i.cacheTokens || 0}` },
1181
- { label: 'Prompt', render: i => `${i.promptTokens || 0}` },
1182
- { label: 'Generated', render: i => `${i.tokensGenerated || 0}` },
1183
- { label: 'Gen Speed', render: i => `${i.tokensPerSecond?.toFixed(1) || '-'} t/s` },
1184
- { label: 'Prompt Speed', render: i => `${i.promptPerSecond?.toFixed(1) || '-'} t/s` },
1185
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1186
- {
1187
- label: 'Status', render: i => {
1188
- if (!i.success) return '<span class="badge badge-error">Failed</span>'
1189
- if (i.interrupted) return '<span class="badge badge-warning">Interrupted</span>'
1190
- return '<span class="badge badge-success">Success</span>'
1191
- }
1192
- },
1193
- ])
1194
-
1195
- // ONNX-STT
1196
- const hasOnnxStt = (onnxStt.generators?.length > 0) ||
1197
- (onnxStt.history?.modelLoads?.length > 0) ||
1198
- (onnxStt.history?.transcriptions?.length > 0)
1199
- document.getElementById('onnxSttCard').style.display = hasOnnxStt ? '' : 'none'
1200
- renderOnnxGenerators('onnxSttGenerators', 'onnxSttGeneratorCount', onnxStt.generators || [], 'ONNX STT')
1201
-
1202
- renderHistory('onnxSttModelHistory', onnxStt.history?.modelLoads || [], [
1203
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1204
- { label: 'Model', render: i => formatModelName(i.repoId) },
1205
- { label: 'DType', render: i => escapeHtml(typeof i.dtype === 'object' ? JSON.stringify(i.dtype) : i.dtype || '-') },
1206
- { label: 'Provider', render: i => `<span class="badge ${i.provider === 'cpu' ? 'badge-info' : 'badge-success'}">${escapeHtml(i.provider || '-')}</span>` },
1207
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1208
- { label: 'Status', render: i => i.success ?
1209
- '<span class="badge badge-success">Success</span>' :
1210
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1211
- },
1212
- ])
1213
-
1214
- renderHistory('onnxSttTranscriptionHistory', onnxStt.history?.transcriptions || [], [
1215
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1216
- { label: 'Model', render: i => formatModelName(i.repoId) },
1217
- { label: 'Provider', render: i => escapeHtml(i.provider || '-') },
1218
- { label: 'Segments', render: i => i.segmentCount ?? '-' },
1219
- { label: 'Text Len', render: i => i.textLength ?? '-' },
1220
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1221
- { label: 'Status', render: i => i.success ?
1222
- '<span class="badge badge-success">Success</span>' :
1223
- `<span class="badge badge-error">Failed</span>`
1224
- },
1225
- ])
1226
-
1227
- // ONNX-TTS
1228
- const hasOnnxTts = (onnxTts.generators?.length > 0) ||
1229
- (onnxTts.history?.modelLoads?.length > 0) ||
1230
- (onnxTts.history?.syntheses?.length > 0)
1231
- document.getElementById('onnxTtsCard').style.display = hasOnnxTts ? '' : 'none'
1232
- renderOnnxGenerators('onnxTtsGenerators', 'onnxTtsGeneratorCount', onnxTts.generators || [], 'ONNX TTS')
1233
-
1234
- renderHistory('onnxTtsModelHistory', onnxTts.history?.modelLoads || [], [
1235
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1236
- { label: 'Model', render: i => formatModelName(i.repoId) },
1237
- { label: 'DType', render: i => escapeHtml(typeof i.dtype === 'object' ? JSON.stringify(i.dtype) : i.dtype || '-') },
1238
- { label: 'Provider', render: i => `<span class="badge ${i.provider === 'cpu' ? 'badge-info' : 'badge-success'}">${escapeHtml(i.provider || '-')}</span>` },
1239
- { label: 'Vocoder', render: i => i.vocoderRepoId ? formatModelName(i.vocoderRepoId) : '-' },
1240
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1241
- { label: 'Status', render: i => i.success ?
1242
- '<span class="badge badge-success">Success</span>' :
1243
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1244
- },
1245
- ])
1246
-
1247
- renderHistory('onnxTtsSynthesisHistory', onnxTts.history?.syntheses || [], [
1248
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1249
- { label: 'Model', render: i => formatModelName(i.repoId) },
1250
- { label: 'Provider', render: i => escapeHtml(i.provider || '-') },
1251
- { label: 'Text Len', render: i => i.textLength ?? '-' },
1252
- { label: 'Duration', render: i => i.cached ? '<span class="badge badge-info">Cached</span>' : `${(i.durationMs / 1000).toFixed(1)}s` },
1253
- { label: 'Status', render: i => i.success ?
1254
- '<span class="badge badge-success">Success</span>' :
1255
- `<span class="badge badge-error">Failed</span>`
1256
- },
1257
- ])
1258
-
1259
- // GGML-TTS
1260
- const hasGgmlTts = (ggmlTts.generators?.length > 0) ||
1261
- (ggmlTts.history?.modelLoads?.length > 0) ||
1262
- (ggmlTts.history?.syntheses?.length > 0)
1263
- document.getElementById('ggmlTtsCard').style.display = hasGgmlTts ? '' : 'none'
1264
- renderGgmlTtsGenerators(ggmlTts.generators || [])
1265
-
1266
- renderHistory('ggmlTtsModelHistory', ggmlTts.history?.modelLoads || [], [
1267
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1268
- { label: 'Model', render: i => formatModelName(i.repoId) },
1269
- { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1270
- { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1271
- { label: 'Vocoder', render: i => i.vocoderRepoId ? formatModelName(i.vocoderRepoId) : '-' },
1272
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1273
- { label: 'Status', render: i => i.success ?
1274
- '<span class="badge badge-success">Success</span>' :
1275
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1276
- },
1277
- ])
1278
-
1279
- renderHistory('ggmlTtsSynthesisHistory', ggmlTts.history?.syntheses || [], [
1280
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1281
- { label: 'Model', render: i => formatModelName(i.repoId) },
1282
- { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1283
- { label: 'Text Len', render: i => i.textLength ?? '-' },
1284
- { label: 'Duration', render: i => i.cached ? '<span class="badge badge-info">Cached</span>' : `${(i.durationMs / 1000).toFixed(1)}s` },
1285
- { label: 'Status', render: i => i.success ?
1286
- '<span class="badge badge-success">Success</span>' :
1287
- `<span class="badge badge-error">Failed</span>`
1288
- },
1289
- ])
1290
-
1291
- renderFunctionsStatus(status.functions || {})
1292
- }
1293
-
1294
- // Local Functions activity (calls, uploads, downloads, auth)
1295
- function renderFunctionsStatus(fns) {
1296
- document.getElementById('functionsCard').style.display = fns.enabled ? '' : 'none'
1297
- if (!fns.enabled) return
1298
-
1299
- const count = fns.count ?? 0
1300
- const daemonCount = fns.daemonCount ?? 0
1301
- document.getElementById('functionsCount').textContent =
1302
- `${count} function${count !== 1 ? 's' : ''}` +
1303
- (daemonCount > 0 ? ` · ${daemonCount} daemon${daemonCount !== 1 ? 's' : ''}` : '')
1304
-
1305
- const c = fns.counters || {}
1306
- const summary = document.getElementById('functionsSummary')
1307
- const stat = (label, value, bad) => `
1308
- <tr>
1309
- <td>${label}</td>
1310
- <td>${value}</td>
1311
- <td>${bad ? `<span class="badge badge-error">${bad}</span>` : '-'}</td>
1312
- </tr>
1313
- `
1314
- withScrollPreserve(summary, () => {
1315
- summary.innerHTML = `
1316
- <div class="table-wrapper">
1317
- <div class="table-inner">
1318
- <table>
1319
- <thead>
1320
- <tr><th>Activity</th><th>Total</th><th>Problems</th></tr>
1321
- </thead>
1322
- <tbody>
1323
- ${stat('Calls', c.calls?.total ?? 0, c.calls?.failed ? `${c.calls.failed} failed` : '')}
1324
- ${stat('Daemon runs', c.daemons?.invocations ?? 0, c.daemons?.failed ? `${c.daemons.failed} failed` : '')}
1325
- ${stat('Uploads', `${c.uploads?.total ?? 0} (${formatBytes(c.uploads?.bytes ?? 0)})`, c.uploads?.failed ? `${c.uploads.failed} failed` : '')}
1326
- ${stat('Downloads', `${c.downloads?.total ?? 0} (${formatBytes(c.downloads?.bytes ?? 0)})`, c.downloads?.missed ? `${c.downloads.missed} missed` : '')}
1327
- ${stat('Auth checks', c.auth?.total ?? 0, c.auth?.denied ? `${c.auth.denied} denied` : '')}
1328
- </tbody>
1329
- </table>
1330
- </div>
1331
- </div>
1332
- `
1333
- })
1334
-
1335
- renderFunctionsDaemons(fns.daemons || [], fns.history?.daemons || [])
1336
-
1337
- const history = fns.history || {}
1338
- const statusBadge = i => i.success ?
1339
- '<span class="badge badge-success">Success</span>' :
1340
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1341
-
1342
- renderHistory('functionsCallHistory', history.calls || [], [
1343
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1344
- { label: 'Function', render: i => escapeHtml(i.name) },
1345
- { label: 'Surface', render: i => `<span class="badge badge-info">${escapeHtml(i.surface || '-')}</span>` },
1346
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(2)}s` },
1347
- { label: 'Status', render: statusBadge },
1348
- ])
1349
-
1350
- renderHistory('functionsUploadHistory', history.uploads || [], [
1351
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1352
- { label: 'Name', render: i => escapeHtml(i.name) },
1353
- { label: 'Via', render: i => `<span class="badge badge-info">${i.source === 'call' ? 'multipart call' : 'upload'}</span>` },
1354
- { label: 'Size', render: i => formatBytes(i.size ?? 0) },
1355
- { label: 'Status', render: statusBadge },
1356
- ])
1357
-
1358
- renderHistory('functionsDownloadHistory', history.downloads || [], [
1359
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1360
- { label: 'Path', render: i => escapeHtml(i.path) },
1361
- { label: 'Size', render: i => i.success ? formatBytes(i.size ?? 0) : '-' },
1362
- { label: 'Status', render: i => i.success ?
1363
- '<span class="badge badge-success">Served</span>' :
1364
- '<span class="badge badge-error">Not found</span>'
1365
- },
1366
- ])
1367
-
1368
- renderHistory('functionsAuthHistory', history.auth || [], [
1369
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1370
- { label: 'Request', render: i => escapeHtml(`${i.method || ''} ${i.path || ''}`.trim()) },
1371
- { label: 'Mode', render: i => escapeHtml(i.mode || '-') },
1372
- { label: 'Subject', render: i => escapeHtml(i.subject || '-') },
1373
- { label: 'Outcome', render: i => i.outcome === 'allowed' ?
1374
- '<span class="badge badge-success">Allowed</span>' :
1375
- `<span class="badge badge-error">${escapeHtml(i.outcome || 'denied')}</span>`
1376
- },
1377
- ])
1378
- }
1379
-
1380
- // Live daemon state + recent daemon event invocations
1381
- function renderFunctionsDaemons(daemons, activity) {
1382
- const hasDaemons = daemons.length > 0
1383
- document.getElementById('functionsDaemonsSection').style.display = hasDaemons ? '' : 'none'
1384
- document.getElementById('functionsDaemonHistorySection').style.display =
1385
- hasDaemons || activity.length > 0 ? '' : 'none'
1386
-
1387
- if (hasDaemons) {
1388
- const container = document.getElementById('functionsDaemons')
1389
- const stateBadge = d => d.state === 'running' ?
1390
- '<span class="badge badge-success">Running</span>' :
1391
- `<span class="badge badge-error">Error${d.error ? `: ${escapeHtml(d.error)}` : ''}</span>`
1392
- const bankBadge = d => {
1393
- if (!d.bankSubscriptions) return '-'
1394
- const cls = d.bank === 'connected' ? 'badge-success' :
1395
- d.bank === 'connecting' ? 'badge-info' : 'badge-warning'
1396
- return `<span class="badge ${cls}">${escapeHtml(d.bank || 'connecting')} (${d.bankSubscriptions})</span>`
1397
- }
1398
- withScrollPreserve(container, () => {
1399
- container.innerHTML = `
1400
- <div class="table-wrapper">
1401
- <div class="table-inner">
1402
- <table>
1403
- <thead>
1404
- <tr>
1405
- <th>Daemon</th><th>State</th><th>Started</th><th>Timers</th>
1406
- <th>Bank</th><th>Events</th><th>Runs</th>
1407
- </tr>
1408
- </thead>
1409
- <tbody>
1410
- ${daemons.map(d => `
1411
- <tr>
1412
- <td title="${escapeHtml(d.description || '')}">${escapeHtml(d.name)}</td>
1413
- <td>${stateBadge(d)}</td>
1414
- <td><span class="timestamp">${d.startedAt ? formatRelativeTime(d.startedAt) : '-'}</span></td>
1415
- <td>${d.timers ?? 0}</td>
1416
- <td>${bankBadge(d)}</td>
1417
- <td>${d.listening ? '<span class="badge badge-info">listening</span>' : '-'}</td>
1418
- <td>${d.counts?.runs ?? 0}${d.counts?.failed ? ` <span class="badge badge-error">${d.counts.failed} failed</span>` : ''}</td>
1419
- </tr>
1420
- `).join('')}
1421
- </tbody>
1422
- </table>
1423
- </div>
1424
- </div>
1425
- `
1426
- })
1427
- }
1428
-
1429
- renderHistory('functionsDaemonHistory', activity, [
1430
- { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1431
- { label: 'Daemon', render: i => escapeHtml(i.name) },
1432
- { label: 'Event', render: i => `<span class="badge badge-info">${escapeHtml(i.event || '-')}</span>` },
1433
- { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(2)}s` },
1434
- { label: 'Status', render: i => i.success ?
1435
- '<span class="badge badge-success">Success</span>' :
1436
- `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1437
- },
1438
- ])
1439
- }
1440
-
1441
- // Fallback: Fetch status via HTTP polling
1442
- let pollingInterval = null
1443
-
1444
- async function fetchStatus() {
1445
- try {
1446
- setConnectionStatus('connecting')
1447
- const response = await fetch('/buttress/status')
1448
- const data = await response.json()
1449
- // Direct JSON response (not tRPC wrapped)
1450
- if (data.ggmlLlm || data.ggmlStt || data.mlxLlm || data.onnxStt || data.onnxTts || data.ggmlTts) {
1451
- updateUI(data)
1452
- setConnectionStatus('connected')
1453
- }
1454
- } catch (error) {
1455
- console.error('Failed to fetch status:', error)
1456
- setConnectionStatus('error')
1457
- }
1458
- }
1459
-
1460
- function startPolling() {
1461
- console.log('[Status] Starting polling')
1462
- if (pollingInterval) clearInterval(pollingInterval)
1463
- fetchStatus()
1464
- pollingInterval = setInterval(fetchStatus, 3000)
1465
- }
1466
-
1467
- function stopPolling() {
1468
- if (pollingInterval) {
1469
- clearInterval(pollingInterval)
1470
- pollingInterval = null
1471
- }
1472
- }
1473
-
1474
- // Manual refresh button
1475
- async function refreshStatus() {
1476
- await fetchStatus()
1477
- }
1478
-
1479
- // Initialize: Start polling
1480
- function init() {
1481
- startPolling()
1482
- }
1483
-
1484
- // Cleanup on page unload
1485
- window.addEventListener('beforeunload', () => {
1486
- stopPolling()
1487
- })
1488
-
1489
- // Start
1490
- init()
1491
- </script>
182
+ <div id="app"><main><p class="empty-page">Loading status…</p></main></div>
183
+ <noscript><p class="empty-page">This page needs JavaScript. The raw data is at <a href="/buttress/status">/buttress/status</a>.</p></noscript>
184
+ <script type="module">var Mt=Object.create;var{getPrototypeOf:At,defineProperty:et,getOwnPropertyNames:It}=Object;var tt=Object.prototype.hasOwnProperty;function Rt(e){return this[e]}var Vt,Nt,Ot=(e,t,n)=>{var s=e!=null&&typeof e==="object";if(s){var r=t?Vt??=new WeakMap:Nt??=new WeakMap,o=r.get(e);if(o)return o}n=e!=null?Mt(At(e)):{};let i=t||!e||!e.__esModule||!tt.call(e,"default")?et(n,"default",{value:e,enumerable:!0}):n;if(e&&typeof e==="object"||typeof e==="function"){for(let u of It(e))if(!tt.call(i,u))et(i,u,{get:Rt.bind(e,u),enumerable:!0})}if(s)r.set(e,i);return i};var Dt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var ot=Dt(function(Wn,st){var se=1000,oe=se*60,re=oe*60,Q=re*24,Bt=Q*7,qt=Q*365.25;st.exports=function(e,t){t=t||{};var n=typeof e;if(n==="string"&&e.length>0)return Et(e);else if(n==="number"&&isFinite(e))return t.long?Gt(e):Pt(e);throw Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))};function Et(e){if(e=String(e),e.length>100)return;var t=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(!t)return;var n=parseFloat(t[1]),s=(t[2]||"ms").toLowerCase();switch(s){case"years":case"year":case"yrs":case"yr":case"y":return n*qt;case"weeks":case"week":case"w":return n*Bt;case"days":case"day":case"d":return n*Q;case"hours":case"hour":case"hrs":case"hr":case"h":return n*re;case"minutes":case"minute":case"mins":case"min":case"m":return n*oe;case"seconds":case"second":case"secs":case"sec":case"s":return n*se;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}function Pt(e){var t=Math.abs(e);if(t>=Q)return Math.round(e/Q)+"d";if(t>=re)return Math.round(e/re)+"h";if(t>=oe)return Math.round(e/oe)+"m";if(t>=se)return Math.round(e/se)+"s";return e+"ms"}function Gt(e){var t=Math.abs(e);if(t>=Q)return Ce(e,t,Q,"day");if(t>=re)return Ce(e,t,re,"hour");if(t>=oe)return Ce(e,t,oe,"minute");if(t>=se)return Ce(e,t,se,"second");return e+" ms"}function Ce(e,t,n,s){var r=t>=n*1.5;return Math.round(e/n)+" "+s+(r?"s":"")}});var G=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e)&&!(e instanceof Date);var Se={"ggml-llm":"LLM · GGML","mlx-llm":"LLM · MLX","ggml-stt":"STT · GGML","ggml-tts":"TTS · GGML","ggml-decision":"Decision · GGML","onnx-stt":"STT · ONNX","onnx-tts":"TTS · ONNX"},Ee=0.85,nt=0.5;var Ft=Ot(ot(),1),Le=1024,Ge=Le*1024,be=Ge*1024,Kn={b:1,kb:Le,mb:Ge,gb:be,tb:be*1024,pb:be*1024*1024};function j(e){if(!Number.isFinite(e))return"—";if(e<0)return`-${j(-e)}`;if(e>=0.1*be){let t=e/be;return`${Number(t.toFixed(2))>=10?t.toFixed(1):t.toFixed(2)} GB`}if(Math.round(e/Le)>=1024)return`${rt(e/Ge)} MB`;if(Math.round(e)>=1024)return`${rt(e/Le)} KB`;return`${Math.round(e)} B`}var rt=(e)=>{let t=e.toFixed(1);return Number(t)>=10?String(Math.round(e)):t};function ae(e){if(!Number.isFinite(e))return"—";if(e<0)return`-${ae(-e)}`;if(Math.round(e)<1000)return`${Math.round(e)} ms`;let t=Number((e/1000).toFixed(1));if(t<60)return`${t} s`;let n=Math.round(e/1000),[s,r,o,i]=[Math.floor(n/86400),Math.floor(n/3600)%24,Math.floor(n/60)%60,n%60];if(s>0)return Pe([s,"d"],[r,"h"]);if(r>0)return Pe([r,"h"],[o,"m"]);return Pe([o,"m"],[i,"s"])}var Pe=(e,t)=>t[0]>0?`${e[0]} ${e[1]} ${t[0]} ${t[1]}`:`${e[0]} ${e[1]}`;var F="—",_t=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Fe=(e)=>String(e).padStart(2,"0"),Ht=(e,t)=>e.getFullYear()===t.getFullYear()&&e.getMonth()===t.getMonth()&&e.getDate()===t.getDate();function z(e){if(typeof e==="number")return Number.isFinite(e)?e:null;if(typeof e!=="string"||e.length===0)return null;let t=Date.parse(e);return Number.isNaN(t)?null:t}function Y(e,t,n={}){let s=new Date(e),r=`${Fe(s.getHours())}:${Fe(s.getMinutes())}`;if(!Ht(s,new Date(t)))return`${_t[s.getMonth()]} ${s.getDate()} ${r}`;return n.seconds===!1?r:`${r}:${Fe(s.getSeconds())}`}function at(e,t){let n=Math.floor((t-e)/1000);if(n<1)return"just now";if(n<60)return`${n} s ago`;if(n<3600)return`${Math.floor(n/60)} m ago`;if(n<86400)return`${Math.floor(n/3600)} h ago`;return`${Math.floor(n/86400)} d ago`}function ye(e){if(!Number.isFinite(e)||e<0)return F;if(e<1000)return`${Number((e/1000).toFixed(2))} s`;if(e<60000)return`${(e/1000).toFixed(1)} s`;return ae(e)}function it(e){if(e<60000)return`${Math.max(0,Math.floor(e/1000))} s`;return ae(e)}function _e(e){return j(e).replace(/(\.\d*?)0+ GB$/,"$1 GB").replace(". GB"," GB")}function L(e){return e.toLocaleString("en-US",{maximumFractionDigits:0})}function He(e){return e!==null&&Number.isFinite(e)&&e>0?e.toFixed(1):F}function Me(e){let t=He(e);return t===F?F:`${t} t/s`}function Je(e){return`${Math.floor(Math.min(1,Math.max(0,e))*100)} %`}function X(e,t,n=`${t}s`){return`${L(e)} ${e===1?t:n}`}function ie(e){let t=e.slice(e.lastIndexOf("/")+1);return t.replace(/[-_.]gguf$/i,"")||t}function Ue(e,t=120){return e.length>t?`${e.slice(0,t-1)}…`:e}function lt(e){return e.slice(Math.max(e.lastIndexOf("/"),e.lastIndexOf("\\"))+1)}var Te=3000,Jt="port answered, but not with a Buttress id",$e=`every ${Te/1000} s`,Ut=20,jt=3,zt=600000,Ae=["ggml-llm","ggml-stt","ggml-tts","mlx-llm","ggml-decision","onnx-stt","onnx-tts"],Yt={"ggml-llm":"ggmlLlm","ggml-stt":"ggmlStt","ggml-tts":"ggmlTts","mlx-llm":"mlxLlm","ggml-decision":"ggmlDecision","onnx-stt":"onnxStt","onnx-tts":"onnxTts"},T=(e)=>G(e)?e:{},S=(e)=>Array.isArray(e)?e.map(T):[],a=(e)=>typeof e==="string"&&e.length>0?e:null,mt=(e)=>Array.isArray(e)?e.map(a).filter((t)=>t!==null):[],v=(e)=>typeof e==="number"&&Number.isFinite(e)?e:null,ce=(e)=>Ae.includes(e),Z=(e,t)=>e.reduce((n,s)=>n+(v(s[t])??0),0),k=(...e)=>e.filter(Boolean).join(" · "),E=(e,t={})=>({text:e??F,...t}),m=(e)=>E(e,{mono:!0}),M=(e)=>{let t=v(e);return m(t===null?null:L(t))},x=(e,t,n)=>({text:e,badge:{text:e,tone:t},...n?{sub:n}:{}}),I=(e,t)=>{let n=z(e);return{text:n===null?F:Y(n,t),mono:!0,tone:"muted"}},P=(e)=>{let t=v(e);return m(t===null?null:ye(t))},Ye=(e)=>{let t=v(e);return m(t?j(t):null)},Ie=(e)=>{let t=v(e);return m(t===null?null:`${L(t)} chars`)},C=(e,t)=>{let n=a(e);return E(n?ie(n):null,t?{sub:t}:{})},Re=(e)=>e===!0?x("yes","ok"):x("no","neutral"),Oe=(e)=>{let t=a(e.error);return t?Ue(t):null},B=(e,t="ok")=>e.success===!1?x("failed","err",Oe(e)):x(t,"ok"),ut=(e)=>m(a(e)??(e&&typeof e==="object"?JSON.stringify(e):null)),ct=(e)=>{let t=a(e);return t?x(t,t==="cpu"?"neutral":"ok"):E(null)},de=x("Loaded","ok"),Wt=x("Loading","accent"),ee=x("Not loaded","muted"),l=(e,t,n={})=>({key:e,label:t,...n}),g={mono:!0},V=l("when","When",g),A=l("model","Model",{grow:!0}),U=l("quant","Quant",g),_=l("variant","Variant",g),W=l("state","State"),q=l("took","Took",g),O=l("result","Result"),Ve=l("gpu","GPU"),we=l("size","Size",g),gt=(e)=>{let t=new Map;return e.map((n)=>{let s=t.get(n.key)??0;return t.set(n.key,s+1),s===0?n:{...n,key:`${n.key}~${s}`}})},ue=(e,t,n)=>gt(e.map((s)=>({key:t(s),cells:n(s)}))),N=(e,t,n,s,r)=>({title:e,table:{columns:n,rows:ue(S(t).slice(0,Ut),(o)=>`${a(o.timestamp)??""}|${a(o.id)??""}`,s),emptyText:r}}),Kt=(e)=>e.generators.rows.length>0||e.histories.some(({table:t})=>t.rows.length>0),Qt=(e)=>{if(e.success===!1)return x("failed","err",Oe(e));if(e.interrupted===!0)return x("interrupted","warn");if(e.contextFull===!0)return x("context full","warn");return x("ok","ok")},ft=(e,t,n)=>N("Recent completions",e,[V,A,l("prompt","Prompt",g),l("generated","Generated",g),l("speed","Speed",g),q,O,l("cache","Cache",g),l("total","Total",g),l("promptSpeed","Prompt speed",g),...n?[U]:[]],(s)=>{let r=[s.cacheTokens,s.promptTokens,s.tokensGenerated].map(v),o=r.every((i)=>i===null)?null:r.reduce((i,u)=>i+(u??0),0);return{when:I(s.timestamp,t),model:C(s.repoId),quant:m(a(s.quantization)),cache:M(s.cacheTokens),prompt:M(s.promptTokens),generated:M(s.tokensGenerated),total:M(o),speed:m(Me(v(s.tokensPerSecond))),promptSpeed:m(Me(v(s.promptPerSecond))),took:P(s.durationMs),result:Qt(s)}},"No completions yet"),pe="Model loads",me="No model loads yet",je=(e,t)=>e===null||t===null||e.toLowerCase()===t.toLowerCase(),ht=(e,t,n)=>je(e.repoId,t)&&je(e.quantization,n);function Xt(e){if(e.overBudget)return x("Over budget","err",e.overBudget);if(e.cached===!0)return x("Not loaded · in cache","muted");if(e.cached===!1)return x("Not loaded · not downloaded","muted");return ee}var Zt=(e)=>x(e.progress===null?"Downloading":`Downloading ${Je(e.progress)}`,"accent");function ge(e,t,n){let s=new Set(n.generators.map((f)=>a(f.repoId))),r=t.downloads.filter((f)=>!s.has(f.repoId)),o=Object.fromEntries(n.columns.map(({key:f})=>[f,E(null)])),i=t.configured.filter((f)=>!n.generators.some((R)=>ht(f,a(R.repoId),a(R.quantization)))&&!t.downloads.some((R)=>je(f.repoId,R.repoId))),u=n.columns.some((f)=>f.key==="quant"),h=[...ue(n.generators,(f)=>a(f.id)??"",n.cellsOf),...ue(r,(f)=>`download|${f.repoId}|${f.filename??""}`,(f)=>({...o,model:C(f.repoId,k(f.repoId,f.filename)),state:Zt(f)})),...ue(i,(f)=>`configured|${f.repoId??""}|${f.quantization??""}`,(f)=>({...o,model:f.repoId?C(f.repoId,f.repoId):E("Default model"),...u?{quant:m(f.quantization)}:{},state:Xt(f)}))],c=n.generators.filter(n.isLoaded).length,d=k(h.length>0&&X(h.length,"generator"),c>0&&`${c} loaded`,r.length>0&&`${r.length} downloading`);return{key:e,title:Se[e],...d?{subtitle:d}:{},...n.badge?{badge:n.badge}:{},generators:{columns:n.columns,rows:h,emptyText:"No generators loaded yet. A client loads them on first use."},...n.stats?{stats:n.stats}:{},...n.requests?{requests:n.requests}:{},histories:n.histories}}var en=(e,t)=>{let n=a(t.id),s=`${e}:${a(t.repoId)??""}:`;return n?.startsWith(s)?n.slice(s.length):null},Ne=(e)=>S(e.contexts).some((t)=>t.hasModel===!0),tn={generating:{text:"generating",tone:"ok"},processing_prompt:{text:"prompt",tone:"accent"},queued:{text:"queued",tone:"muted"},done:{text:"done",tone:"neutral"}},nn=(e)=>S(e.parallelStatuses).flatMap((t)=>S(t.requests)).filter((t)=>t.state==="generating");function sn(e,t){return{columns:[l("request","Request",g),A,l("type","Type"),W,l("prompt","Prompt",g),l("tokens","Tokens",g),l("speed","Speed",g),l("elapsed","Elapsed",g)],rows:ue(e,(n)=>`${a(n.generatorId)??""}|${String(n.request_id??"")}`,(n)=>{let s=a(n.state)??"",r=tn[s]??{text:s||F,tone:"neutral"},o=(v(n.prompt_ms)??0)+(v(n.generation_ms)??0);return{request:m(n.request_id===void 0?null:`#${String(n.request_id)}`),model:C(t.get(n.generatorId)),type:E(a(n.type)),state:x(r.text,r.tone),prompt:M(n.prompt_length),tokens:M(n.tokens_generated),speed:m(Me(v(n.tokens_per_second))),elapsed:m(o>0?ye(o):null)}}),emptyText:"No requests in flight"}}function on(e){let{data:t,now:n}=e,s=S(t.generators),r=S(t.parallelStatuses),o=(y)=>r.filter((w)=>w.generatorId===y.id),i=new Map(s.map((y)=>[y.id,y.repoId])),u=T(t.history),h=r.flatMap((y)=>S(y.requests).map((w)=>({...w,generatorId:y.generatorId}))),c=h.filter((y)=>y.state==="generating"),d=Z(r,"queued_requests"),f;if(c.length>0)f={text:`${c.length} generating`,tone:"ok"};else if(d>0)f={text:`${L(d)} queued`,tone:"accent"};let R=r.length>0?[{value:L(Z(r,"n_parallel")),label:"parallel slots"},{value:L(Z(r,"active_slots")),label:"active"},{value:L(d),label:"queued"},{value:He(c.length>0?Z(c,"tokens_per_second"):null),label:"tokens / s now"}]:void 0;return ge("ggml-llm",e,{columns:[A,U,_,l("context","Context",g),l("slots","Slots",g),l("sessions","Sessions",g),W],generators:s,cellsOf:(y)=>{let w=o(y),H=ee;if(Ne(y))H=de;else if(S(y.contexts).length>0)H=Wt;return{model:C(y.repoId,k(a(y.repoId),en("ggml-llm",y))),quant:m(a(y.quantization)),variant:m(a(y.variant)),context:M(y.nCtx),slots:m(w.length>0?`${Z(w,"active_slots")} / ${Z(w,"n_parallel")}`:null),sessions:M(y.refCount),state:H}},isLoaded:Ne,badge:f,stats:R,requests:r.length>0?sn(h,i):void 0,histories:[ft(u.completions,n,!0),N(pe,u.modelLoads,[V,A,_,q,O,U,l("context","Context",g),l("gpuLayers","GPU layers",g)],(y)=>({when:I(y.timestamp,n),model:C(y.repoId),quant:m(a(y.quantization)),variant:m(a(y.variant)),context:M(y.nCtx),gpuLayers:M(y.nGpuLayers),took:P(y.durationMs),result:B(y)}),me)]})}function rn(e){let{data:t,now:n}=e,s=S(t.generators),r=s.map((c)=>T(c.queueStatus)),o=r.filter((c)=>c.processing===!0).length,i=Z(r,"queuedCount"),u=T(t.history),h=(c)=>{let d=v(c.queuedCount)??0;if(c.processing===!0)return k("busy",d>0&&`${d} queued`);return d>0?`${d} queued`:"idle"};return ge("ggml-stt",e,{columns:[A,l("modelType","Type",g),U,_,l("queue","Queue",g),W],generators:s,cellsOf:(c)=>({model:C(c.repoId,a(c.repoId)),modelType:m(a(c.modelType)),quant:m(a(c.quantization)),variant:m(a(c.variant)),queue:m(h(T(c.queueStatus))),state:c.hasContext===!0?de:ee}),isLoaded:(c)=>c.hasContext===!0,badge:s.length>0?{text:`queue ${L(i)}`,tone:i>0?"accent":"muted"}:void 0,stats:s.length>0?[{value:L(o),label:"processing"},{value:L(i),label:"queued"}]:void 0,histories:[N("Recent transcriptions",u.transcriptions,[V,A,l("text","Text",g),q,O,l("segments","Segments",g),l("modelType","Type",g),U,_],(c)=>({when:I(c.timestamp,n),model:C(c.repoId),modelType:m(a(c.modelType)),quant:m(a(c.quantization)),variant:m(a(c.variant)),segments:M(c.segmentCount),text:Ie(c.textLength),took:P(c.durationMs),result:B(c)}),"No transcriptions yet"),N(pe,u.modelLoads,[V,A,_,q,O,l("modelType","Type",g),U,Ve],(c)=>({when:I(c.timestamp,n),model:C(c.repoId),modelType:m(a(c.modelType)),quant:m(a(c.quantization)),variant:m(a(c.variant)),gpu:Re(c.useGpu),took:P(c.durationMs),result:B(c)}),me)]})}var bt=(e)=>({took:e.cached===!0?E(null):P(e.durationMs),result:e.cached===!0?B(e,"ok · cached"):B(e)});function an(e){let{data:t,now:n}=e,s=T(t.history),r=(o)=>{let i=a(o);return m(i?ie(i):null)};return ge("ggml-tts",e,{columns:[A,U,l("vocoder","Vocoder",g),_,Ve,we,W],generators:S(t.generators),cellsOf:(o)=>({model:C(o.repoId,k(a(o.repoId),a(o.family))),quant:m(a(o.quantization)),vocoder:r(o.vocoderRepoId),variant:m(a(o.variant)),gpu:Re(o.useGpu),size:Ye(o.modelBytes),state:o.hasContext===!0?de:ee}),isLoaded:(o)=>o.hasContext===!0,histories:[N("Recent syntheses",s.syntheses,[V,A,l("text","Text",g),l("audio","Audio",g),q,O,_],(o)=>{let i=v(o.audioLength),u=v(o.samplingRate);return{when:I(o.timestamp,n),model:C(o.repoId),variant:m(a(o.variant)),text:Ie(o.textLength),audio:m(i!==null&&u?ye(i/u*1000):null),...bt(o)}},"No syntheses yet"),N(pe,s.modelLoads,[V,A,_,q,O,U,l("vocoder","Vocoder",g),Ve],(o)=>({when:I(o.timestamp,n),model:C(o.repoId),quant:m(a(o.quantization)),variant:m(a(o.variant)),vocoder:r(o.vocoderRepoId),gpu:Re(o.useGpu),took:P(o.durationMs),result:B(o)}),me)]})}function ln(e){let{data:t,now:n}=e,s=T(t.history);return ge("mlx-llm",e,{columns:[A,_,l("contexts","Contexts",g),l("sessions","Sessions",g),W],generators:S(t.generators),cellsOf:(r)=>({model:C(r.repoId,a(r.repoId)),variant:m(a(r.variant)??"mlx"),contexts:M(S(r.contexts).length),sessions:M(r.refCount),state:Ne(r)?de:ee}),isLoaded:Ne,histories:[ft(s.completions,n,!1),N(pe,s.modelLoads,[V,A,_,q,O],(r)=>({when:I(r.timestamp,n),model:C(r.repoId),variant:m(a(r.variant)??"mlx"),took:P(r.durationMs),result:B(r)}),me)]})}function un(e){let{data:t,now:n}=e,s=T(t.history);return ge("ggml-decision",e,{columns:[A,l("decisionType","Type",g),U,_,Ve,we,W],generators:S(t.generators),cellsOf:(r)=>{let o=a(r.mmprojFilename);return{model:C(r.repoId,k(a(r.repoId),o&&`+ ${o}`)),decisionType:m(a(r.decisionType)),quant:m(a(r.quantization)),variant:m(a(r.variant)),gpu:Re(r.useGpu),size:Ye(r.modelBytes),state:r.hasContext===!0?de:ee}},isLoaded:(r)=>r.hasContext===!0,histories:[N("Recent decisions",s.decisions,[V,A,l("questions","Questions",g),q,O,l("images","Images",g),l("tokens","Tokens",g)],(r)=>({when:I(r.timestamp,n),model:C(r.repoId),questions:M(r.questionCount),images:M(v(r.imageCount)||null),tokens:M(r.inputTokens),took:P(r.durationMs),result:B(r)}),"No decisions yet"),N(pe,s.modelLoads,[V,A,_,q,O,l("decisionType","Type",g),U],(r)=>({when:I(r.timestamp,n),model:C(r.repoId),decisionType:m(a(r.decisionType)),quant:m(a(r.quantization)),variant:m(a(r.variant)),took:P(r.durationMs),result:B(r)}),me)]})}function dt(e,t){let{data:n,now:s}=t,r=T(n.history),o=e==="onnx-tts",i=(d)=>{let f=a(d);return f?ie(f):null},u=l("provider","Provider"),h=o?N("Recent syntheses",r.syntheses,[V,A,l("text","Text",g),q,O,u],(d)=>({when:I(d.timestamp,s),model:C(d.repoId),provider:E(a(d.provider)),text:Ie(d.textLength),...bt(d)}),"No syntheses yet"):N("Recent transcriptions",r.transcriptions,[V,A,l("text","Text",g),q,O,l("segments","Segments",g),u],(d)=>({when:I(d.timestamp,s),model:C(d.repoId),provider:E(a(d.provider)),segments:M(d.segmentCount),text:Ie(d.textLength),took:P(d.durationMs),result:B(d)}),"No transcriptions yet"),c=(d)=>(v(d.pipelines)??0)>0;return ge(e,t,{columns:[A,l("dtype","DType",g),u,l("device","Device",g),we,l("pipelines","Pipelines",g),W],generators:S(n.generators),cellsOf:(d)=>{let f=i(d.vocoderRepoId);return{model:C(d.repoId,k(a(d.repoId),f&&`+ ${f}`)),dtype:ut(d.dtype),provider:ct(d.provider),device:m(a(d.device)),size:Ye(d.modelBytes),pipelines:M(d.pipelines??0),state:c(d)?de:ee}},isLoaded:c,histories:[h,N(pe,r.modelLoads,[V,A,u,q,O,l("dtype","DType",g),...o?[l("vocoder","Vocoder",g)]:[]],(d)=>({when:I(d.timestamp,s),model:C(d.repoId),dtype:ut(d.dtype),provider:ct(d.provider),vocoder:m(i(d.vocoderRepoId)),took:P(d.durationMs),result:B(d)}),me)]})}var cn={"ggml-llm":on,"ggml-stt":rn,"ggml-tts":an,"mlx-llm":ln,"ggml-decision":un,"onnx-stt":(e)=>dt("onnx-stt",e),"onnx-tts":(e)=>dt("onnx-tts",e)},ve=(e,t,n,s)=>({value:L(e),label:k(t,n>0&&`${L(n)} ${s}`)}),dn={connected:"ok",connecting:"accent"};function pn(e,t,n){let s=T(e.counters),[r,o,i,u,h]=["calls","daemons","uploads","downloads","auth"].map((p)=>T(s[p])),c=S(e.daemons),d=T(e.history),f=S(d.daemons),R=v(e.count)??0,y=v(e.daemonCount)??0,w=(p,D)=>v(p[D])??0,H=l("name","Function",{mono:!0,grow:!0});return{key:"functions",title:"Local functions",subtitle:k(X(R,"function"),y>0&&X(y,"daemon"),n&&"hot reload on"),badge:{text:"experimental",tone:"warn"},generators:{columns:[l("name","Daemon",{mono:!0,grow:!0}),W,l("started","Started",g),l("timers","Timers",g),l("bank","Bank"),l("events","Events"),l("runs","Runs",g)],rows:ue(c,(p)=>a(p.name)??"",(p)=>{let D=T(p.counts),J=w(D,"failed"),he=w(p,"bankSubscriptions"),ne=a(p.bank)??"connecting",Ze=a(p.description);return{name:E(a(p.name),{mono:!0,...Ze?{sub:Ze}:{}}),state:p.state==="running"?x("running","ok"):x("error","err",Oe(p)),started:I(p.startedAt,t),timers:M(w(p,"timers")),bank:he>0?x(`${ne} · ${he}`,dn[ne]??"warn"):E(null),events:p.listening===!0?x("listening","accent"):E(null),runs:E(k(L(w(D,"runs")),J>0&&`${L(J)} failed`),{mono:!0,...J>0?{tone:"err"}:{}})}}),emptyText:"No daemons running"},stats:[ve(w(r,"total"),"calls",w(r,"failed"),"failed"),ve(w(o,"invocations"),"daemon runs",w(o,"failed"),"failed"),ve(w(i,"total"),`uploads · ${j(w(i,"bytes"))}`,w(i,"failed"),"failed"),ve(w(u,"total"),`downloads · ${j(w(u,"bytes"))}`,w(u,"missed"),"missed"),ve(w(h,"total"),"auth checks",w(h,"denied"),"denied")],histories:[...c.length>0||f.length>0?[N("Daemon activity",f,[V,l("name","Daemon",g),l("event","Event"),q,O],(p)=>({when:I(p.timestamp,t),name:m(a(p.name)),event:x(a(p.event)??F,"accent"),took:P(p.durationMs),result:B(p)}),"No daemon activity yet")]:[],N("Function calls",d.calls,[V,H,l("surface","Surface"),q,O],(p)=>({when:I(p.timestamp,t),name:m(a(p.name)),surface:x(a(p.surface)??F,"accent"),took:P(p.durationMs),result:B(p)}),"No function calls yet"),N("Uploads",d.uploads,[V,l("name","Name",{mono:!0,grow:!0}),l("via","Via"),we,O],(p)=>({when:I(p.timestamp,t),name:m(a(p.name)),via:x(p.source==="call"?"multipart call":"upload","neutral"),size:m(j(w(p,"size"))),result:B(p)}),"No uploads yet"),N("Downloads",d.downloads,[V,l("path","Path",{mono:!0,grow:!0}),we,O],(p)=>({when:I(p.timestamp,t),path:m(a(p.path)),size:m(p.success===!1?null:j(w(p,"size"))),result:p.success===!1?x("not found","err"):x("served","ok")}),"No downloads yet"),N("Auth checks",d.auth,[V,l("request","Request",{mono:!0,grow:!0}),l("mode","Mode",g),l("subject","Subject",g),l("outcome","Outcome")],(p)=>{let D=a(p.outcome)??"denied";return{when:I(p.timestamp,t),request:m([a(p.method),a(p.path)].filter(Boolean).join(" ")||null),mode:m(a(p.mode)),subject:m(a(p.subject)),outcome:D==="allowed"?x("allowed","ok"):x(D,"err")}},"No auth checks yet")]}}function mn(e){let{probe:t,config:n,now:s}=e,r=n?`:${n.port}`:"";switch(t.state){case"live":return{state:"live",label:`Live · polling ${$e}`,tone:"ok",lastUpdatedLabel:t.lastAt===null?null:Y(t.lastAt,s)};case"connecting":return{state:"connecting",label:n?`Connecting · ${r}`:"Connecting…",tone:"muted",lastUpdatedLabel:null};case"mismatch":return{state:"mismatch",label:n?`Port ${n.port} used by another server`:"Another server answered",tone:"warn",lastUpdatedLabel:null};case"unreachable":return n?{state:"unreachable",label:`Not running · probing ${r}`,tone:"muted",lastUpdatedLabel:null}:{state:"unreachable",label:`Disconnected · retrying ${$e}`,tone:"err",lastUpdatedLabel:null}}}function gn(e,t){let{probe:n,config:s,now:r}=e,o=[],i=z(T(t?.server).startedAt);if(n.state==="live"&&s){let{mtimeMs:u}=s,h=u!==null&&i!==null&&u>i?`${lt(s.path)} was saved at ${Y(u,r,{seconds:!1})}; the server loaded it at ${Y(i,r,{seconds:!1})}.`:null,c=s.pendingTarget??null,d=c?`The saved file moves the server ${[c.port!==s.port&&`to :${c.port}`,c.expectedId!==s.expectedId&&`to id ${c.expectedId}`].filter(Boolean).join(" and ")}; this window follows the running one until it stops.`:null;if(h||d)o.push({key:"config-changed",tone:"accent",title:"Config changed since this server started.",body:[h,d,"Restart to apply."].filter(Boolean).join(" "),command:s.startCommand})}if(s&&n.state==="unreachable"&&n.error===Jt)o.push({key:"port-busy",tone:"warn",title:`Port ${s.port} is used by another program`,body:"Something answers HTTP on this port without a Buttress id: another app, or a Buttress too old to report one. Change [server] port in the config, or stop that program."});if(n.state==="mismatch"){let u=n.otherServer,h=s?` This config expects ${s.expectedId}.`:"";o.push({key:"port-in-use",tone:"warn",title:s?`Port ${s.port} is used by another Buttress`:"This port is used by another Buttress",body:u?`It reports id ${u.id}, name "${u.name}", version ${u.version}.${h} Change [server] port in the config, or stop that server.`:`Another server answers on this port.${h} Change [server] port in the config, or stop that server.`})}if(!s&&n.state==="unreachable"){let u=z(t?.timestamp);o.push(t?{key:"disconnected",tone:"err",title:"Lost connection to the server.",body:k(u!==null&&`Showing data from ${Y(u,r)}`,`retrying ${$e}`)}:{key:"disconnected",tone:"err",title:"Cannot reach the server.",body:k(n.error,`retrying ${$e}`)})}return o}var fn=(e)=>S(e).flatMap((t)=>ce(t.type)?[{type:t.type,repoId:a(t.repoId),quantization:a(t.quantization)}]:[]);function hn(e,t){if(!e)return t??[];if(!t)return e;return e.map((n)=>{let s=t.find((r)=>r.type===n.type&&ht(r,n.repoId,n.quantization));return s?{...n,cached:s.cached,overBudget:s.overBudget}:n})}function bn(e){let t=T(e?.server),n=T(t.host),s=a(n.cpuModel),r=v(n.totalMemoryBytes),o=a(n.gpuName);return{generatorTypes:Array.isArray(t.generatorTypes)?t.generatorTypes.filter(ce):null,features:G(t.features)?t.features:null,caps:Array.isArray(t.caps)?ze(t.caps):null,generators:Array.isArray(t.generators)?fn(t.generators):null,host:s&&r!==null?{cpuModel:s,cpuCores:v(n.cpuCores)??0,totalMemoryBytes:r,appleSilicon:n.appleSilicon===!0,...o?{gpuName:o}:{}}:null}}var le=(e)=>({label:e,value:"off",tone:"muted"}),yn=[{key:"openaiCompat",label:"OpenAI-compatible",path:"/oai-compat/v1"},{key:"anthropicMessages",label:"Anthropic Messages",path:"/anthropic-messages"},{key:"typesafeCompat",label:"TypeSafe System One",path:"/typesafe-compat"}];function vn(e){let t=[],n=e.autodiscover;if(G(n)){let o=v(n.udpPort),i=a(n.httpPath),u=o!==null&&(n.udpAnnouncing===!1?`UDP ${o} (not announcing: unbound)`:`UDP ${o}`);t.push({label:"Autodiscover",value:k(u,i&&`HTTP ${i}`)||"on · no transport enabled"})}else t.push(le("Autodiscover"));for(let o of yn)t.push(e[o.key]===!0?{label:o.label,value:"on",tone:"ok",detail:o.path}:le(o.label));let s=T(e.functions);if(s.enabled===!0){let o=v(s.daemons)??0;t.push({label:"Local functions",value:k(`${L(v(s.count)??0)} loaded`,o>0&&X(o,"daemon"),s.hotReload===!0&&"hot reload")})}else t.push(le("Local functions"));let r=e.agents;if(G(r)&&r.enabled===!0){let o=mt(r.names);t.push({label:"Agents",value:k(`${L(v(r.count)??o.length)} configured`,o.length>0&&Ue(o.join(", "),80))})}else t.push(le("Agents"));return t}function $n(e,t){let n=[{label:"Autodiscover",value:e?"HTTP /buttress/info":"HTTP info not found at /buttress/info"}],s=T(t?.functions);if(t){let o=v(s.daemonCount)??0;n.push(s.enabled===!0?{label:"Local functions",value:k(`${L(v(s.count)??0)} loaded`,o>0&&X(o,"daemon"))}:le("Local functions"))}let r=e?.agents;if(G(r))n.push(r.enabled===!0?{label:"Agents",value:`${L(v(r.count)??0)} configured`}:le("Agents"));return n}function wn(e,t,n,s){let r=T(t?.server),o=a(e?.id)??a(r.id);if(!o)return null;let{now:i}=s,u=v(e?.port)??v(r.port)??s.config?.port??0,h=T(e?.authentication),c=T(e?.workspace),d=z(r.startedAt),f=v(r.uptimeMs),R=v(T(r.sessions).open),y=nn(T(t?.ggmlLlm)).length,w=mt(e?.addresses),H=[];if(e)H.push({label:"Authentication",value:h.required===!0?"JWT required on data paths":"none",tone:h.required===!0?"ok":"muted"});return H.push(...n.features?vn(n.features):$n(e,t)),{name:a(e?.name)??a(r.name)??o,id:o,version:`v${a(e?.version)??a(r.version)??"?"}`,addresses:w.map((p)=>`http://${p}:${u}`),port:u,bound:e?h.bound===!0:null,workspaceLabel:a(c.name)??a(c.id),kid:a(h.kid),startedLabel:d===null?null:Y(d,i,{seconds:!1}),uptimeLabel:f===null?null:`up ${it(f)}`,sessionsLabel:R===null?null:k(`${L(R)} open`,y>0&&`${y} generating`),features:H}}function Tn(e,t){if(e.score<=0||!t)return e.usableBytes;if(t.appleSilicon&&e.type==="mlx-llm")return t.totalMemoryBytes;if(t.appleSilicon&&e.hasGpu)return t.totalMemoryBytes*Ee;if(e.hasGpu||e.usableBytes>0)return e.usableBytes;return t.totalMemoryBytes*nt}var xn=(e,t)=>{if(e.deviceName)return e.deviceName;if(e.hasGpu)return t?.gpuName??"GPU";return t?.cpuModel??"CPU only"};function kn(e,t,n){return[...e].sort((s,r)=>Ae.indexOf(s.type)-Ae.indexOf(r.type)).map((s)=>{let r=t.has(s.type),o=s.score>0,i;if(!r)i=o?"Available; no generator configured":"Not available on this host";else if(!o)i="Not detected on this host";else if(s.variant)i=`variant ${s.variant}`;let u="neutral";if(!r)u="muted";else if(!o)u="err";else if(s.hasGpu)u="ok";let h=Tn(s,n);return{type:s.type,title:Se[s.type],score:r?s.score:null,hasGpu:s.hasGpu,usableLabel:h>0?_e(h):F,deviceLabel:xn(s,n),...i?{note:i}:{},tone:u}})}function Sn(e,t){if(!e)return;let n=_e(e.totalMemoryBytes);if(e.appleSilicon)return k(e.gpuName??e.cpuModel,`${n} unified`,t&&`usable at the backend default fraction ${Ee}`);return k(e.cpuModel,e.cpuCores>0&&X(e.cpuCores,"core"),`${n} RAM`)}var ze=(e)=>S(e).flatMap((t)=>ce(t.type)?[{type:t.type,score:v(t.score)??0,hasGpu:t.hasGpu===!0,usableBytes:v(t.usableBytes)??0}]:[]),yt=(e)=>S(e.active).flatMap((t)=>{let n=a(t.repoId);if(!n)return[];let s=v(t.progress);return[{type:ce(t.type)?t.type:null,repoId:n,filename:a(t.filename),progress:s===null?null:Math.min(1,Math.max(0,s)),startedAt:z(t.startedAt)}]}),pt=(e,t)=>k(t,e&&Se[e]);function Cn(e,t){let n=yt(e).map((o)=>{let i=o.startedAt!==null&&`running ${ae(Math.max(0,t-o.startedAt))}`;return{key:`active|${o.repoId}|${o.filename??""}`,title:o.filename??ie(o.repoId),subtitle:pt(o.type,o.repoId),progress:o.progress,detail:k(o.progress===null?"waiting for the first progress report":Je(o.progress),i),tone:"accent"}}),r=S(e.recent).filter((o)=>{if(o.success===!1)return!0;let i=z(o.finishedAt);return i!==null&&t-i<zt}).slice(0,jt).map((o)=>{let i=a(o.repoId)??F,u=z(o.finishedAt),h=v(o.durationMs),c=o.success!==!1;return{key:`recent|${a(o.finishedAt)??""}|${i}`,title:a(o.filename)??ie(i),subtitle:pt(ce(o.type)?o.type:null,i),progress:null,detail:k(c?"done":"failed",h!==null&&`${c?"in":"after"} ${ye(h)}`,u!==null&&Y(u,t),!c&&Oe(o)),tone:c?"ok":"err"}});return gt([...n,...r])}function Ln(e){let{probe:t,config:n,now:s}=e;if(!n||t.state==="live")return null;let r=t.otherServer??null,o=t.error??"no answer";if(t.state==="connecting")o="probing…";else if(t.state==="mismatch")o=`answered by ${r?`"${r.name}" (${r.id})`:"another server"}`;return{probeUrl:`http://127.0.0.1:${n.port}/buttress/info`,resultLabel:t.state==="connecting"?o:k(o,t.lastAt!==null&&at(t.lastAt,s),$e),expectedId:n.expectedId,command:n.startCommand,otherServer:r}}function vt(e){let{probe:t,config:n,now:s}=e,r=t.state==="live"||n===null&&e.status!==null,o=r&&e.status!==null?T(e.status):null,i=r&&e.info!==null?T(e.info):null,u=bn(o),h=T(o?.downloads),c=yt(h),d=new Set([...u.generatorTypes??[...n?.generatorTypes??[],...i?ze(i.generators).map((D)=>D.type):[]],...[...S(h.active),...S(h.recent)].map((D)=>D.type).filter(ce)]),f=i?ze(i.generators):[],R=[];if(u.caps&&u.caps.length>0)R=u.caps;else if(f.length>0)R=f;else if(e.localCaps)R=e.localCaps;let y=u.host??e.hostInfo??null,w=Sn(y,R.length>0),H=hn(u.generators,n?.generators),p=[];if(o){for(let J of Ae){let he=cn[J]({data:T(o[Yt[J]]),downloads:c.filter((ne)=>ne.type===J),configured:H.filter((ne)=>ne.type===J),now:s});if(d.has(J)||Kt(he))p.push(he)}let D=T(o.functions);if(D.enabled===!0){let J=T(u.features?.functions).hotReload===!0;p.push(pn(D,s,J))}}return{connection:mn(e),banners:gn(e,o),identity:r?wn(i,o,u,e):null,hardware:kn(R,d,y),...w?{hardwareSummary:w}:{},downloads:o?Cn(h,s):[],sections:p,notRunning:Ln(e)}}var Mn=new Set(["ggml-llm","mlx-llm","functions"]),fe={logo:'<svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2.5" y="3" width="15" height="6" rx="1.5"/><rect x="2.5" y="11" width="15" height="6" rx="1.5"/><circle cx="6" cy="6" r="0.8" fill="currentColor"/><circle cx="6" cy="14" r="0.8" fill="currentColor"/></svg>',refresh:'<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M16 10a6 6 0 1 1-1.8-4.3"/><path d="M16 3v4h-4"/></svg>',chevron:'<svg class="chev" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>',info:'<svg class="banner-icon" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="10" cy="10" r="7.5"/><path d="M10 9v5"/><circle cx="10" cy="6.5" r="0.6" fill="currentColor"/></svg>',warning:'<svg class="banner-icon" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M10 3.5 2.5 16.5h15L10 3.5Z"/><path d="M10 8v4"/><circle cx="10" cy="14.2" r="0.6" fill="currentColor"/></svg>',stopped:'<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="10" cy="10" r="7"/><path d="M5.5 5.5l9 9"/></svg>'},An={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},b=(e)=>String(e).replace(/[&<>"']/g,(t)=>An[t]),$t=(e)=>e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""),K=(e,t)=>`<span class="badge tone-${t}">${b(e)}</span>`;function In(e){let[t,...n]=e.label.split(" · "),s=[...n,e.lastUpdatedLabel].filter(Boolean).join(" · "),r=s?`<span class="pill-detail"> · ${b(s)}</span>`:"";return`<header class="top">
185
+ <div class="brand">${fe.logo}<h1>Buttress Status</h1></div>
186
+ <span class="pill tone-${e.tone}"><span class="dot" aria-hidden="true"></span><span>${b(t)}</span>${r}</span>
187
+ <button type="button" class="btn" id="refresh" data-action="refresh" aria-label="Refresh now">${fe.refresh}<span class="btn-label">Refresh</span></button>
188
+ </header>`}function Rn(e){let t=e.tone==="warn"||e.tone==="err"?fe.warning:fe.info,n=e.command?`<code class="banner-cmd">${b(e.command)}</code>`:"";return`<div class="banner tone-${e.tone}">${t}<p class="banner-text"><strong>${b(e.title)}</strong> ${b(e.body)}</p>${n}</div>`}function Vn(e){return`<section class="card not-running" aria-labelledby="not-running-title">
189
+ <div class="nr-head"><span class="nr-icon">${fe.stopped}</span><div><h2 id="not-running-title">No server is running for this config</h2><p class="help">This page switches to live status by itself when one starts.</p></div></div>
190
+ <dl class="nr-facts">
191
+ <div><dt>Probing</dt><dd class="mono">${b(e.probeUrl)}</dd></div>
192
+ <div><dt>Result</dt><dd>${b(e.resultLabel)}</dd></div>
193
+ <div><dt>Expected server id</dt><dd class="mono">${b(e.expectedId)}</dd></div>
194
+ </dl>
195
+ <div class="nr-start"><span class="nr-label">Start it from a terminal</span><pre class="cmd-block">${b(e.command)}</pre><p class="help">This page does not start or stop servers.</p></div>
196
+ </section>`}function Nn(e){let t="";if(e.bound===!0)t=K(e.workspaceLabel?`Bound · ${e.workspaceLabel}`:"Bound","ok");else if(e.bound===!1)t=K("Not bound","muted");let n=[e.id,e.kid&&`issuer kid ${e.kid}`].filter(Boolean).join(" · "),s=e.addresses.length?`<ul class="chips" aria-label="Addresses">${e.addresses.map((o)=>`<li class="chip">${b(o)}</li>`).join("")}</ul>`:"",r=[];if(e.startedLabel)r.push(["Started",b(e.startedLabel)]);if(e.sessionsLabel)r.push(["Sessions",b(e.sessionsLabel)]);r.push(["Port",b(e.port)]);for(let o of e.features){let i=o.tone?K(o.value,o.tone):b(o.value),u=o.detail?` <span class="mono help fact-detail">${b(o.detail)}</span>`:"";r.push([o.label,i+u])}return`<section class="card identity" aria-labelledby="identity-name">
197
+ <div class="identity-main">
198
+ <div class="identity-title"><h2 id="identity-name">${b(e.name)}</h2>${K(e.version,"muted")}${t}${e.uptimeLabel?K(e.uptimeLabel,"muted"):""}</div>
199
+ <p class="mono help identity-id">${b(n)}</p>
200
+ ${s}
201
+ </div>
202
+ <dl class="facts">${r.map(([o,i])=>`<div class="fact"><dt>${b(o)}</dt><dd>${i}</dd></div>`).join("")}</dl>
203
+ </section>`}function On(e,t){let n=e.map((o)=>{let i=o.score===null?K("no generator",o.tone):`<span class="badge tone-${o.tone}"><span class="score-word">score </span>${b(o.score)}</span>`,u=[o.deviceLabel,`usable ${o.usableLabel}`,o.note].filter(Boolean).map((h)=>b(h)).join(" · ");return`<li class="cap"><div class="cap-head"><strong>${b(o.title)}</strong>${i}</div><span class="help">${u}</span></li>`}).join(""),s=e.length?b(t?`per backend · ${t}`:"per backend, as reported by the server"):b(t??""),r=e.length?`<ul class="caps">${n}</ul>`:'<p class="help">No per-backend scores: the server has no generators, or is too old to report them without /buttress/info.</p>';return`<section class="hardware" aria-labelledby="hardware-title">
204
+ <div class="block-head"><h2 id="hardware-title">Hardware</h2><span class="help">${s}</span></div>
205
+ ${r}
206
+ </section>`}function Dn(e){let t=e.tone==="accent",n="";if(t&&e.progress!==null){let r=Math.round(e.progress*100);n=`<div class="bar" role="progressbar" aria-label="${b(e.title)}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${r}"><span style="width:${r}%"></span></div>`}else if(t)n=`<div class="bar indeterminate" role="progressbar" aria-label="${b(e.title)}"><span></span></div>`;let s=t?"":` t-${e.tone}`;return`<li class="dl">
207
+ <div class="dl-head"><span class="dl-title mono">${b(e.title)}</span><span class="help">${b(e.subtitle)}</span></div>
208
+ ${n}
209
+ <span class="help mono${s}">${b(e.detail)}</span>
210
+ </li>`}function Bn(e){let t=e.filter((s)=>s.tone==="accent").length;return`<section class="card" aria-labelledby="downloads-title">
211
+ <div class="card-head"><h2 id="downloads-title">Downloads</h2><span class="help">${t>0?`${t} in progress`:"none in progress"}</span></div>
212
+ <ul class="dl-list">${e.map(Dn).join("")}</ul>
213
+ </section>`}function qn(e,t){if(!e)return"<td>—</td>";let n=[e.mono||t?"mono":"",e.tone?`t-${e.tone}`:""].filter(Boolean).join(" "),s=b(e.text);if(e.badge)s=K(e.badge.text,e.badge.tone);else if(e.sub)s=`<span class="primary">${s}</span>`;let r=e.sub?`<span class="cell-sub" title="${b(e.sub)}">${b(e.sub)}</span>`:"";return`<td${n?` class="${n}"`:""}>${s}${r}</td>`}function We(e,t,n){if(e.rows.length===0)return`<p class="empty">${b(e.emptyText)}</p>`;let s=e.columns.map((o)=>{let i=[o.mono?"mono":"",o.grow?"grow":""].filter(Boolean),u=o.width?` style="min-width:${o.width}px"`:"";return`<th scope="col"${i.length?` class="${i.join(" ")}"`:""}${u}>${b(o.label)}</th>`}).join(""),r=e.rows.map((o)=>`<tr>${e.columns.map((i)=>qn(o.cells[i.key],i.mono)).join("")}</tr>`).join("");return`<div class="table-box" id="tbl-${$t(t)}" data-scroll-key="${b(t)}" data-label="${b(n)}"><table><thead><tr>${s}</tr></thead><tbody>${r}</tbody></table></div>`}function En(e,t){let n=`sec-${$t(e.key)}`,s=e.subtitle?`<span class="help sec-sub">${b(e.subtitle)}</span>`:'<span class="sec-sub"></span>',r=e.badge?K(e.badge.text,e.badge.tone):"",o=e.stats?.length?`<ul class="stats" aria-label="${b(e.title)} totals">${e.stats.map((h)=>`<li class="stat"><b>${b(h.value)}</b><span>${b(h.label)}</span></li>`).join("")}</ul>`:"",i=e.requests?`<div class="block"><h3 class="block-title">Requests in flight</h3>${We(e.requests,`${e.key}/requests`,`${e.title} requests`)}</div>`:"",u=e.histories.length?`<div class="split">${e.histories.map((h)=>`<div class="block"><h3 class="block-title">${b(h.title)}</h3>${We(h.table,`${e.key}/${h.title}`,`${e.title} ${h.title}`)}</div>`).join("")}</div>`:"";return`<section class="card sec${Mn.has(e.key)?" wide":""}" aria-labelledby="${n}-title">
214
+ <h2 class="sec-head"><button type="button" class="card-toggle" id="${n}-toggle" data-action="toggle" data-key="${b(e.key)}" aria-expanded="${!t}" aria-controls="${n}-body"><span class="sec-title" id="${n}-title">${b(e.title)}</span>${s}${r}${fe.chevron}</button></h2>
215
+ <div class="sec-body" id="${n}-body"${t?" hidden":""}>
216
+ ${We(e.generators,`${e.key}/generators`,`${e.title} generators`)}
217
+ ${o}${i}${u}
218
+ </div>
219
+ </section>`}function Pn(e){if(e.sections.length>0||e.notRunning)return"";if(e.identity)return'<p class="empty-page">No generators are configured or loaded yet.</p>';if(e.connection.state==="connecting")return'<p class="empty-page">Connecting to the server…</p>';return""}function Gn(e){let t=`every ${Te/1000} s`;if(e===null)return`Polls /buttress/status ${t}. This server serves no /buttress/info (autodiscover HTTP is off).`;if(e)return`Polls /buttress/status and ${e} ${t}.`;return`Polls /buttress/status ${t}, and /buttress/info while the server serves it.`}function wt(e,t){let n=e.sections.length?`<div class="sections">${e.sections.map((r)=>En(r,t.collapsed.has(r.key))).join("")}</div>`:"",s=[e.banners.map(Rn).join(""),e.notRunning?Vn(e.notRunning):"",e.identity?Nn(e.identity):"",e.hardware.length||e.hardwareSummary?On(e.hardware,e.hardwareSummary):"",e.downloads.length?Bn(e.downloads):"",n,Pn(e),`<p class="help foot">${b(Gn(t.infoPath))}</p>`].join(`
220
+ `);return`${In(e.connection)}
221
+ <main>${s}</main>`}var kt="buttress-status:collapsed",Fn=1e4,_n=30000,xe=document.getElementById("app"),De=null,St=null,Qe={state:"connecting",lastAt:null},te=Hn(),Tt,Ke=!1,Be=0,ke;function Hn(){try{let e=JSON.parse(localStorage.getItem(kt)??"[]");return new Set(Array.isArray(e)?e.filter((t)=>typeof t==="string"):[])}catch{return new Set}}function Jn(){try{localStorage.setItem(kt,JSON.stringify([...te]))}catch{}}async function Ct(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Fn);try{let s=await fetch(e,{cache:"no-store",headers:{Accept:"application/json"},signal:t.signal});if(!s.ok)return{ok:!1,error:`HTTP ${s.status}`,httpStatus:s.status};return{ok:!0,data:await s.json()}}catch{return{ok:!1,error:t.signal.aborted?"timed out":"network error"}}finally{clearTimeout(n)}}function Un(e){let t=e.server,n=G(t)?t.features:void 0;if(!G(n))return;let s=n.autodiscover;return G(s)&&typeof s.httpPath==="string"?s.httpPath:null}function jn(e){let t=ke===void 0?"/buttress/info":ke;if(t===null||!e&&Date.now()<Be)return Promise.resolve(null);return Ct(t)}async function qe(e=!1){if(Ke)return;Ke=!0,clearTimeout(Tt);try{let[t,n]=await Promise.all([Ct("/buttress/status"),jn(e)]),s=Date.now();if(t.ok&&G(t.data)){St=t.data;let r=Un(t.data);if(r===null)De=null;else if(n?.ok&&G(n.data))De=n.data,Be=0;else if(n)De=null,Be=n.ok||n.httpStatus!==void 0?s+_n:0;if(r!==ke)Be=0;ke=r,Qe={state:"live",lastAt:s}}else Qe={state:"unreachable",lastAt:s,error:t.ok?"unexpected response":t.error};Xe()}finally{Ke=!1,Tt=setTimeout(qe,Te)}}function Lt(){for(let e of xe.querySelectorAll("[data-scroll-key]"))if(e.scrollWidth>e.clientWidth+1)e.tabIndex=0,e.setAttribute("role","region"),e.setAttribute("aria-label",e.dataset.label??"");else e.removeAttribute("tabindex"),e.removeAttribute("role"),e.removeAttribute("aria-label")}function Xe(){let e=vt({info:De,status:St,probe:Qe,config:null,localCaps:null,now:Date.now()}),t=new Map;for(let s of xe.querySelectorAll("[data-scroll-key]"))t.set(s.dataset.scrollKey??"",s.scrollLeft);let n=document.activeElement?.id;xe.innerHTML=wt(e,{collapsed:te,infoPath:ke});for(let s of xe.querySelectorAll("[data-scroll-key]")){let r=t.get(s.dataset.scrollKey??"");if(r)s.scrollLeft=r}if(Lt(),n)document.getElementById(n)?.focus({preventScroll:!0});document.title=e.identity?`${e.identity.name} · Buttress Status`:"Buttress Status"}xe.addEventListener("click",(e)=>{let t=e.target instanceof Element?e.target.closest("[data-action]"):null;if(!(t instanceof HTMLElement))return;let{action:n,key:s}=t.dataset;if(n==="refresh")qe(!0);else if(n==="toggle"&&s){if(te=new Set(te),te.has(s))te.delete(s);else te.add(s);Jn(),Xe()}});var xt=0;window.addEventListener("resize",()=>{cancelAnimationFrame(xt),xt=requestAnimationFrame(Lt)});document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="visible")qe()});Xe();qe();
222
+ </script>
1492
223
  </body>
1493
-
1494
- </html>
224
+ </html>