@lovenyberg/ove 0.3.0 → 0.4.0

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.
@@ -0,0 +1,519 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Ove — Status</title>
7
+ <link rel="icon" href="/favicon.ico">
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+
11
+ :root {
12
+ --bg: #1a1a1a;
13
+ --bg-panel: #161616;
14
+ --bg-item: #1e1e1e;
15
+ --bg-item-hover: #252525;
16
+ --border: #2a2a2a;
17
+ --border-light: #333;
18
+ --text: #e0e0e0;
19
+ --text-dim: #777;
20
+ --text-muted: #555;
21
+ --accent: #8ab4f8;
22
+ --green: #4ade80;
23
+ --green-dim: #16361f;
24
+ --red: #f28b82;
25
+ --red-dim: #3b1a1a;
26
+ --amber: #fbbf24;
27
+ --amber-dim: #3b2e0a;
28
+ --cyan: #22d3ee;
29
+ --cyan-dim: #0a2e33;
30
+ }
31
+
32
+ body {
33
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
34
+ background: var(--bg);
35
+ color: var(--text);
36
+ height: 100vh;
37
+ display: flex;
38
+ flex-direction: column;
39
+ overflow: hidden;
40
+ }
41
+
42
+ header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ padding: 0.5rem 1rem;
47
+ border-bottom: 1px solid var(--border);
48
+ background: var(--bg-panel);
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ .header-left {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 1rem;
56
+ }
57
+
58
+ .header-logo {
59
+ width: 24px;
60
+ height: 24px;
61
+ border-radius: 3px;
62
+ object-fit: cover;
63
+ }
64
+
65
+ .header-left h1 {
66
+ font-size: 0.85rem;
67
+ font-weight: 600;
68
+ letter-spacing: 0.05em;
69
+ color: var(--text-dim);
70
+ }
71
+
72
+ .header-left h1 span { color: var(--text); }
73
+
74
+ .nav-link {
75
+ color: var(--text-dim);
76
+ text-decoration: none;
77
+ font-size: 0.7rem;
78
+ padding: 0.2rem 0.5rem;
79
+ border: 1px solid var(--border);
80
+ border-radius: 3px;
81
+ transition: all 0.15s;
82
+ }
83
+ .nav-link:hover { color: var(--text); border-color: var(--border-light); }
84
+
85
+ .header-right {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.75rem;
89
+ }
90
+
91
+ .toggle-wrap {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 0.4rem;
95
+ font-size: 0.7rem;
96
+ color: var(--text-dim);
97
+ cursor: pointer;
98
+ user-select: none;
99
+ }
100
+
101
+ .toggle-wrap input { display: none; }
102
+
103
+ .toggle-track {
104
+ width: 28px;
105
+ height: 14px;
106
+ background: var(--border);
107
+ border-radius: 7px;
108
+ position: relative;
109
+ transition: background 0.2s;
110
+ }
111
+
112
+ .toggle-track::after {
113
+ content: '';
114
+ position: absolute;
115
+ top: 2px;
116
+ left: 2px;
117
+ width: 10px;
118
+ height: 10px;
119
+ background: var(--text-dim);
120
+ border-radius: 50%;
121
+ transition: all 0.2s;
122
+ }
123
+
124
+ .toggle-wrap input:checked + .toggle-track {
125
+ background: var(--green-dim);
126
+ }
127
+
128
+ .toggle-wrap input:checked + .toggle-track::after {
129
+ left: 16px;
130
+ background: var(--green);
131
+ }
132
+
133
+ .last-updated {
134
+ font-size: 0.6rem;
135
+ color: var(--text-muted);
136
+ font-variant-numeric: tabular-nums;
137
+ }
138
+
139
+ /* ── Content ── */
140
+ .content {
141
+ flex: 1;
142
+ overflow-y: auto;
143
+ padding: 1rem;
144
+ scrollbar-width: thin;
145
+ scrollbar-color: var(--border) transparent;
146
+ }
147
+
148
+ .section-header {
149
+ font-size: 0.6rem;
150
+ color: var(--text-muted);
151
+ text-transform: uppercase;
152
+ letter-spacing: 0.1em;
153
+ margin-bottom: 0.5rem;
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0.5rem;
157
+ }
158
+
159
+ .section-header::after {
160
+ content: '';
161
+ flex: 1;
162
+ height: 1px;
163
+ background: var(--border);
164
+ }
165
+
166
+ /* ── Adapter cards ── */
167
+ .adapter-grid {
168
+ display: grid;
169
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
170
+ gap: 0.5rem;
171
+ margin-bottom: 1.5rem;
172
+ }
173
+
174
+ .adapter-card {
175
+ background: var(--bg-item);
176
+ border: 1px solid var(--border);
177
+ border-radius: 3px;
178
+ padding: 0.75rem;
179
+ transition: border-color 0.15s;
180
+ }
181
+
182
+ .adapter-card:hover { border-color: var(--border-light); }
183
+
184
+ .adapter-card-header {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ margin-bottom: 0.4rem;
189
+ }
190
+
191
+ .adapter-name {
192
+ font-size: 0.8rem;
193
+ font-weight: 600;
194
+ color: var(--text);
195
+ }
196
+
197
+ .adapter-type {
198
+ font-size: 0.55rem;
199
+ color: var(--text-muted);
200
+ text-transform: uppercase;
201
+ letter-spacing: 0.05em;
202
+ }
203
+
204
+ .badge {
205
+ font-size: 0.55rem;
206
+ padding: 0.1rem 0.35rem;
207
+ border-radius: 2px;
208
+ font-weight: 600;
209
+ letter-spacing: 0.03em;
210
+ text-transform: uppercase;
211
+ flex-shrink: 0;
212
+ }
213
+
214
+ .badge-connected { background: var(--green-dim); color: var(--green); }
215
+ .badge-disconnected { background: var(--red-dim); color: var(--red); }
216
+ .badge-degraded { background: var(--amber-dim); color: var(--amber); }
217
+ .badge-unknown { background: #222; color: var(--text-muted); }
218
+
219
+ .adapter-error {
220
+ font-size: 0.65rem;
221
+ color: var(--red);
222
+ margin-top: 0.3rem;
223
+ line-height: 1.3;
224
+ word-break: break-word;
225
+ }
226
+
227
+ .adapter-details {
228
+ margin-top: 0.3rem;
229
+ font-size: 0.6rem;
230
+ color: var(--text-muted);
231
+ line-height: 1.4;
232
+ }
233
+
234
+ .adapter-details span { color: var(--text-dim); }
235
+
236
+ .adapter-started {
237
+ font-size: 0.55rem;
238
+ color: var(--text-muted);
239
+ margin-top: 0.3rem;
240
+ font-variant-numeric: tabular-nums;
241
+ }
242
+
243
+ /* ── Queue stats ── */
244
+ .queue-grid {
245
+ display: grid;
246
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
247
+ gap: 0.5rem;
248
+ margin-bottom: 1.5rem;
249
+ }
250
+
251
+ .queue-card {
252
+ background: var(--bg-item);
253
+ border: 1px solid var(--border);
254
+ border-radius: 3px;
255
+ padding: 0.6rem 0.75rem;
256
+ text-align: center;
257
+ }
258
+
259
+ .queue-count {
260
+ font-size: 1.4rem;
261
+ font-weight: 700;
262
+ font-variant-numeric: tabular-nums;
263
+ line-height: 1.2;
264
+ }
265
+
266
+ .queue-count.pending { color: var(--amber); }
267
+ .queue-count.running { color: var(--cyan); }
268
+ .queue-count.completed { color: var(--green); }
269
+ .queue-count.failed { color: var(--red); }
270
+
271
+ .queue-label {
272
+ font-size: 0.6rem;
273
+ color: var(--text-muted);
274
+ text-transform: uppercase;
275
+ letter-spacing: 0.05em;
276
+ margin-top: 0.15rem;
277
+ }
278
+
279
+ /* ── Uptime ── */
280
+ .uptime-row {
281
+ font-size: 0.65rem;
282
+ color: var(--text-muted);
283
+ margin-bottom: 1rem;
284
+ }
285
+
286
+ .uptime-value { color: var(--text-dim); }
287
+
288
+ .empty-state {
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ height: 50vh;
293
+ color: var(--text-muted);
294
+ font-size: 0.8rem;
295
+ }
296
+
297
+ .pairing-code {
298
+ margin-top: 0.5rem;
299
+ padding: 0.5rem;
300
+ background: var(--amber-dim);
301
+ border: 1px solid var(--amber);
302
+ border-radius: 3px;
303
+ }
304
+
305
+ .pairing-code-label {
306
+ font-size: 0.55rem;
307
+ color: var(--amber);
308
+ text-transform: uppercase;
309
+ letter-spacing: 0.05em;
310
+ margin-bottom: 0.25rem;
311
+ }
312
+
313
+ .pairing-code-value {
314
+ font-size: 1.4rem;
315
+ font-weight: 700;
316
+ color: var(--text);
317
+ letter-spacing: 0.15em;
318
+ font-variant-numeric: tabular-nums;
319
+ }
320
+
321
+ .pairing-code-hint {
322
+ font-size: 0.55rem;
323
+ color: var(--text-muted);
324
+ margin-top: 0.25rem;
325
+ line-height: 1.3;
326
+ }
327
+
328
+ .adapter-card[data-status="degraded"] { border-color: var(--amber-dim); }
329
+ .adapter-card[data-status="disconnected"] { border-color: var(--red-dim); }
330
+
331
+ ::-webkit-scrollbar { width: 6px; }
332
+ ::-webkit-scrollbar-track { background: transparent; }
333
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
334
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
335
+ </style>
336
+ </head>
337
+ <body>
338
+ <header>
339
+ <div class="header-left">
340
+ <img src="/logo.png" class="header-logo" alt="Ove">
341
+ <h1><span>ove</span> status</h1>
342
+ <a href="/" class="nav-link">chat</a>
343
+ <a href="/trace" class="nav-link">traces</a>
344
+ </div>
345
+ <div class="header-right">
346
+ <span class="last-updated" id="lastUpdated"></span>
347
+ <label class="toggle-wrap">
348
+ <input type="checkbox" id="autoRefresh" checked />
349
+ <div class="toggle-track"></div>
350
+ auto-refresh
351
+ </label>
352
+ </div>
353
+ </header>
354
+
355
+ <div class="content" id="content">
356
+ <div class="empty-state" id="loading">Loading status...</div>
357
+ </div>
358
+
359
+ <script>
360
+ var API_KEY = localStorage.getItem("ove-api-key") || prompt("API Key:");
361
+ if (API_KEY) localStorage.setItem("ove-api-key", API_KEY);
362
+
363
+ var contentEl = document.getElementById("content");
364
+ var loadingEl = document.getElementById("loading");
365
+ var lastUpdatedEl = document.getElementById("lastUpdated");
366
+ var autoRefreshEl = document.getElementById("autoRefresh");
367
+ var refreshTimer = null;
368
+
369
+ function apiHeaders() { return { "X-API-Key": API_KEY }; }
370
+
371
+ function el(tag, cls, text) {
372
+ var e = document.createElement(tag);
373
+ if (cls) e.className = cls;
374
+ if (text != null) e.textContent = text;
375
+ return e;
376
+ }
377
+
378
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
379
+
380
+ function fmtUptime(secs) {
381
+ var d = Math.floor(secs / 86400);
382
+ var h = Math.floor((secs % 86400) / 3600);
383
+ var m = Math.floor((secs % 3600) / 60);
384
+ var s = Math.floor(secs % 60);
385
+ var parts = [];
386
+ if (d > 0) parts.push(d + "d");
387
+ if (h > 0) parts.push(h + "h");
388
+ if (m > 0) parts.push(m + "m");
389
+ parts.push(s + "s");
390
+ return parts.join(" ");
391
+ }
392
+
393
+ function fmtTime(iso) {
394
+ if (!iso) return "";
395
+ var d = new Date(iso);
396
+ return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
397
+ }
398
+
399
+ function renderStatus(data) {
400
+ clear(contentEl);
401
+
402
+ // Uptime
403
+ var uptimeRow = el("div", "uptime-row");
404
+ uptimeRow.appendChild(document.createTextNode("uptime "));
405
+ uptimeRow.appendChild(el("span", "uptime-value", fmtUptime(data.uptime)));
406
+ contentEl.appendChild(uptimeRow);
407
+
408
+ // Adapters section
409
+ contentEl.appendChild(el("div", "section-header", "Adapters"));
410
+ var grid = el("div", "adapter-grid");
411
+
412
+ for (var i = 0; i < data.adapters.length; i++) {
413
+ var a = data.adapters[i];
414
+ var card = el("div", "adapter-card");
415
+ card.dataset.status = a.status;
416
+
417
+ var hdr = el("div", "adapter-card-header");
418
+ var left = el("div");
419
+ left.appendChild(el("div", "adapter-name", a.name));
420
+ left.appendChild(el("div", "adapter-type", a.type));
421
+ hdr.appendChild(left);
422
+ hdr.appendChild(el("span", "badge badge-" + a.status, a.status));
423
+ card.appendChild(hdr);
424
+
425
+ if (a.error) {
426
+ card.appendChild(el("div", "adapter-error", a.error));
427
+ }
428
+
429
+ if (a.details) {
430
+ // Show pairing code prominently if present
431
+ if (a.details.pairingCode) {
432
+ var pcBlock = el("div", "pairing-code");
433
+ pcBlock.appendChild(el("div", "pairing-code-label", "Pairing code"));
434
+ pcBlock.appendChild(el("div", "pairing-code-value", String(a.details.pairingCode)));
435
+ pcBlock.appendChild(el("div", "pairing-code-hint", "Enter on your phone: WhatsApp > Linked Devices > Link a Device"));
436
+ card.appendChild(pcBlock);
437
+ }
438
+
439
+ var detailKeys = Object.keys(a.details).filter(function (k) { return k !== "pairingCode"; });
440
+ if (detailKeys.length > 0) {
441
+ var details = el("div", "adapter-details");
442
+ for (var j = 0; j < detailKeys.length; j++) {
443
+ var val = a.details[detailKeys[j]];
444
+ if (val === undefined || val === null) continue;
445
+ var item = document.createElement("div");
446
+ item.appendChild(document.createTextNode(detailKeys[j] + " "));
447
+ item.appendChild(el("span", null, Array.isArray(val) ? val.join(", ") : String(val)));
448
+ details.appendChild(item);
449
+ }
450
+ card.appendChild(details);
451
+ }
452
+ }
453
+
454
+ if (a.startedAt) {
455
+ card.appendChild(el("div", "adapter-started", "since " + fmtTime(a.startedAt)));
456
+ }
457
+
458
+ grid.appendChild(card);
459
+ }
460
+ contentEl.appendChild(grid);
461
+
462
+ // Queue section
463
+ contentEl.appendChild(el("div", "section-header", "Queue"));
464
+ var qGrid = el("div", "queue-grid");
465
+ var statuses = ["pending", "running", "completed", "failed"];
466
+ for (var i = 0; i < statuses.length; i++) {
467
+ var s = statuses[i];
468
+ var qCard = el("div", "queue-card");
469
+ qCard.appendChild(el("div", "queue-count " + s, String(data.queue[s] || 0)));
470
+ qCard.appendChild(el("div", "queue-label", s));
471
+ qGrid.appendChild(qCard);
472
+ }
473
+ contentEl.appendChild(qGrid);
474
+
475
+ lastUpdatedEl.textContent = "updated " + new Date().toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
476
+ }
477
+
478
+ async function fetchStatus() {
479
+ try {
480
+ var res = await fetch("/api/status?key=" + encodeURIComponent(API_KEY), { headers: apiHeaders() });
481
+ if (!res.ok) {
482
+ if (loadingEl && loadingEl.parentNode) loadingEl.textContent = "Error: " + res.status;
483
+ return;
484
+ }
485
+ var data = await res.json();
486
+ renderStatus(data);
487
+ } catch (err) {
488
+ if (loadingEl && loadingEl.parentNode) {
489
+ loadingEl.textContent = "Error: " + err.message;
490
+ }
491
+ }
492
+ }
493
+
494
+ function startAutoRefresh() {
495
+ if (refreshTimer) clearInterval(refreshTimer);
496
+ refreshTimer = setInterval(fetchStatus, 5000);
497
+ }
498
+
499
+ function stopAutoRefresh() {
500
+ if (refreshTimer) {
501
+ clearInterval(refreshTimer);
502
+ refreshTimer = null;
503
+ }
504
+ }
505
+
506
+ autoRefreshEl.addEventListener("change", function () {
507
+ if (autoRefreshEl.checked) {
508
+ startAutoRefresh();
509
+ } else {
510
+ stopAutoRefresh();
511
+ }
512
+ });
513
+
514
+ // Init
515
+ fetchStatus();
516
+ startAutoRefresh();
517
+ </script>
518
+ </body>
519
+ </html>