termbeam 0.0.1

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,779 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
8
+ />
9
+ <meta name="apple-mobile-web-app-capable" content="yes" />
10
+ <meta name="mobile-web-app-capable" content="yes" />
11
+ <meta name="theme-color" content="#1a1a2e" />
12
+ <title>TermBeam</title>
13
+ <style>
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+ html,
20
+ body {
21
+ height: 100%;
22
+ width: 100%;
23
+ background: #1a1a2e;
24
+ color: #e0e0e0;
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
26
+ }
27
+
28
+ .header {
29
+ padding: 20px 16px 12px;
30
+ text-align: center;
31
+ border-bottom: 1px solid #0f3460;
32
+ }
33
+ .header h1 {
34
+ font-size: 22px;
35
+ font-weight: 700;
36
+ }
37
+ .header h1 span {
38
+ color: #533483;
39
+ }
40
+ .header p {
41
+ font-size: 13px;
42
+ color: #888;
43
+ margin-top: 4px;
44
+ }
45
+
46
+ .sessions-list {
47
+ padding: 16px;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 12px;
51
+ }
52
+
53
+ .session-card {
54
+ background: #16213e;
55
+ border: 1px solid #0f3460;
56
+ border-radius: 12px;
57
+ padding: 16px;
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 8px;
61
+ text-decoration: none;
62
+ color: inherit;
63
+ transition: transform 0.2s ease;
64
+ cursor: pointer;
65
+ -webkit-tap-highlight-color: transparent;
66
+ position: relative;
67
+ z-index: 1;
68
+ }
69
+ .session-card:hover {
70
+ border-color: #533483;
71
+ }
72
+
73
+ .swipe-wrap {
74
+ position: relative;
75
+ overflow: hidden;
76
+ border-radius: 12px;
77
+ }
78
+ .swipe-delete {
79
+ position: absolute;
80
+ right: 0;
81
+ top: 0;
82
+ bottom: 0;
83
+ width: 80px;
84
+ background: #e74c3c;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ border-radius: 0 12px 12px 0;
89
+ z-index: 0;
90
+ }
91
+ .swipe-delete button {
92
+ background: none;
93
+ border: none;
94
+ color: white;
95
+ font-size: 24px;
96
+ cursor: pointer;
97
+ width: 100%;
98
+ height: 100%;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ gap: 4px;
103
+ flex-direction: column;
104
+ }
105
+ .swipe-delete button span {
106
+ font-size: 11px;
107
+ font-weight: 600;
108
+ }
109
+
110
+ .session-card .top {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ }
115
+ .session-card .name {
116
+ font-size: 17px;
117
+ font-weight: 600;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 8px;
121
+ }
122
+ .session-card .name .dot {
123
+ width: 10px;
124
+ height: 10px;
125
+ border-radius: 50%;
126
+ background: #2ecc71;
127
+ flex-shrink: 0;
128
+ }
129
+ .session-card .pid {
130
+ font-size: 12px;
131
+ color: #888;
132
+ background: #1a1a2e;
133
+ padding: 2px 8px;
134
+ border-radius: 4px;
135
+ }
136
+ .session-card .details {
137
+ display: flex;
138
+ flex-wrap: wrap;
139
+ gap: 6px 16px;
140
+ font-size: 13px;
141
+ color: #aaa;
142
+ }
143
+ .session-card .details span {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 4px;
147
+ }
148
+ .session-card .connect-btn {
149
+ align-self: flex-end;
150
+ background: #533483;
151
+ color: white;
152
+ border: none;
153
+ border-radius: 8px;
154
+ padding: 8px 20px;
155
+ font-size: 14px;
156
+ font-weight: 600;
157
+ cursor: pointer;
158
+ }
159
+ .session-card .connect-btn:active {
160
+ background: #6a42a8;
161
+ }
162
+
163
+ .new-session {
164
+ margin: 0 16px;
165
+ padding: 14px;
166
+ background: transparent;
167
+ border: 2px dashed #0f3460;
168
+ border-radius: 12px;
169
+ color: #888;
170
+ font-size: 15px;
171
+ font-weight: 600;
172
+ cursor: pointer;
173
+ text-align: center;
174
+ transition:
175
+ border-color 0.15s,
176
+ color 0.15s;
177
+ }
178
+ .new-session:active {
179
+ border-color: #533483;
180
+ color: #e0e0e0;
181
+ }
182
+
183
+ .empty-state {
184
+ text-align: center;
185
+ padding: 60px 20px;
186
+ color: #666;
187
+ font-size: 15px;
188
+ }
189
+
190
+ /* New session modal */
191
+ .modal-overlay {
192
+ display: none;
193
+ position: fixed;
194
+ top: 0;
195
+ left: 0;
196
+ right: 0;
197
+ bottom: 0;
198
+ background: rgba(0, 0, 0, 0.7);
199
+ z-index: 100;
200
+ justify-content: center;
201
+ align-items: center;
202
+ }
203
+ .modal-overlay.visible {
204
+ display: flex;
205
+ }
206
+ .modal {
207
+ background: #16213e;
208
+ border-radius: 16px;
209
+ width: 90%;
210
+ max-width: 500px;
211
+ padding: 24px 20px;
212
+ }
213
+ .modal h2 {
214
+ font-size: 18px;
215
+ margin-bottom: 16px;
216
+ }
217
+ .modal label {
218
+ display: block;
219
+ font-size: 13px;
220
+ color: #aaa;
221
+ margin-bottom: 4px;
222
+ margin-top: 12px;
223
+ }
224
+ .modal input,
225
+ .modal select {
226
+ width: 100%;
227
+ padding: 10px 12px;
228
+ background: #1a1a2e;
229
+ border: 1px solid #0f3460;
230
+ border-radius: 8px;
231
+ color: #e0e0e0;
232
+ font-size: 15px;
233
+ outline: none;
234
+ }
235
+ .modal input:focus {
236
+ border-color: #533483;
237
+ }
238
+ .modal-actions {
239
+ display: flex;
240
+ gap: 12px;
241
+ margin-top: 20px;
242
+ }
243
+ .modal-actions button {
244
+ flex: 1;
245
+ padding: 12px;
246
+ border: none;
247
+ border-radius: 8px;
248
+ font-size: 15px;
249
+ font-weight: 600;
250
+ cursor: pointer;
251
+ }
252
+ .btn-cancel {
253
+ background: #0f3460;
254
+ color: #e0e0e0;
255
+ }
256
+ .btn-create {
257
+ background: #533483;
258
+ color: white;
259
+ }
260
+
261
+ /* Folder browser */
262
+ .cwd-picker {
263
+ display: flex;
264
+ gap: 8px;
265
+ }
266
+ .cwd-picker input {
267
+ flex: 1;
268
+ }
269
+ .cwd-browse-btn {
270
+ background: #0f3460;
271
+ color: #e0e0e0;
272
+ border: 1px solid #0f3460;
273
+ border-radius: 8px;
274
+ padding: 0 14px;
275
+ font-size: 18px;
276
+ cursor: pointer;
277
+ flex-shrink: 0;
278
+ display: flex;
279
+ align-items: center;
280
+ }
281
+ .cwd-browse-btn:active {
282
+ background: #533483;
283
+ }
284
+
285
+ .browser-overlay {
286
+ display: none;
287
+ position: fixed;
288
+ top: 0;
289
+ left: 0;
290
+ right: 0;
291
+ bottom: 0;
292
+ background: rgba(0, 0, 0, 0.8);
293
+ z-index: 200;
294
+ justify-content: center;
295
+ align-items: flex-end;
296
+ }
297
+ .browser-overlay.visible {
298
+ display: flex;
299
+ }
300
+ .browser-sheet {
301
+ background: #16213e;
302
+ border-radius: 16px 16px 0 0;
303
+ width: 100%;
304
+ max-width: 500px;
305
+ height: 80vh;
306
+ display: flex;
307
+ flex-direction: column;
308
+ overflow: hidden;
309
+ }
310
+ .browser-header {
311
+ padding: 16px 16px 12px;
312
+ border-bottom: 1px solid #0f3460;
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: space-between;
316
+ }
317
+ .browser-header h3 {
318
+ font-size: 17px;
319
+ font-weight: 600;
320
+ }
321
+ .browser-close {
322
+ background: none;
323
+ border: none;
324
+ color: #888;
325
+ font-size: 24px;
326
+ cursor: pointer;
327
+ padding: 0 4px;
328
+ line-height: 1;
329
+ }
330
+ .browser-close:active {
331
+ color: #e0e0e0;
332
+ }
333
+
334
+ .browser-breadcrumb {
335
+ padding: 8px 16px;
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 2px;
339
+ font-size: 13px;
340
+ overflow-x: auto;
341
+ white-space: nowrap;
342
+ border-bottom: 1px solid #0f3460;
343
+ flex-shrink: 0;
344
+ -webkit-overflow-scrolling: touch;
345
+ }
346
+ .crumb {
347
+ background: none;
348
+ border: none;
349
+ color: #888;
350
+ font-size: 13px;
351
+ cursor: pointer;
352
+ padding: 4px 6px;
353
+ border-radius: 4px;
354
+ flex-shrink: 0;
355
+ }
356
+ .crumb:active,
357
+ .crumb:hover {
358
+ background: #0f3460;
359
+ color: #e0e0e0;
360
+ }
361
+ .crumb.current {
362
+ color: #e0e0e0;
363
+ font-weight: 600;
364
+ }
365
+ .crumb-sep {
366
+ color: #444;
367
+ flex-shrink: 0;
368
+ }
369
+
370
+ .browser-list {
371
+ flex: 1;
372
+ overflow-y: auto;
373
+ padding: 4px 0;
374
+ -webkit-overflow-scrolling: touch;
375
+ }
376
+ .browser-empty {
377
+ text-align: center;
378
+ padding: 40px 20px;
379
+ color: #666;
380
+ font-size: 14px;
381
+ }
382
+ .folder-item {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 12px;
386
+ padding: 12px 16px;
387
+ cursor: pointer;
388
+ border-bottom: 1px solid rgba(15, 52, 96, 0.5);
389
+ transition: background 0.1s;
390
+ -webkit-tap-highlight-color: transparent;
391
+ }
392
+ .folder-item:active {
393
+ background: rgba(83, 52, 131, 0.3);
394
+ }
395
+ .folder-item:hover {
396
+ background: rgba(83, 52, 131, 0.15);
397
+ }
398
+ .folder-icon {
399
+ font-size: 22px;
400
+ flex-shrink: 0;
401
+ width: 28px;
402
+ text-align: center;
403
+ }
404
+ .folder-name {
405
+ font-size: 15px;
406
+ color: #e0e0e0;
407
+ flex: 1;
408
+ overflow: hidden;
409
+ text-overflow: ellipsis;
410
+ white-space: nowrap;
411
+ }
412
+ .folder-arrow {
413
+ color: #444;
414
+ font-size: 18px;
415
+ flex-shrink: 0;
416
+ }
417
+
418
+ .browser-footer {
419
+ padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
420
+ border-top: 1px solid #0f3460;
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: 8px;
424
+ }
425
+ .browser-current-path {
426
+ font-size: 12px;
427
+ color: #888;
428
+ overflow: hidden;
429
+ text-overflow: ellipsis;
430
+ white-space: nowrap;
431
+ }
432
+ .browser-select-btn {
433
+ width: 100%;
434
+ padding: 12px;
435
+ background: #533483;
436
+ color: white;
437
+ border: none;
438
+ border-radius: 10px;
439
+ font-size: 16px;
440
+ font-weight: 600;
441
+ cursor: pointer;
442
+ }
443
+ .browser-select-btn:active {
444
+ background: #6a42a8;
445
+ }
446
+ </style>
447
+ </head>
448
+ <body>
449
+ <div class="header">
450
+ <h1>📡 Term<span>Cast</span></h1>
451
+ <p>Beam your terminal to any device · <span id="version" style="color: #533483"></span></p>
452
+ </div>
453
+
454
+ <div class="sessions-list" id="sessions-list"></div>
455
+ <button class="new-session" id="new-session-btn">+ New Session</button>
456
+
457
+ <div class="modal-overlay" id="modal">
458
+ <div class="modal">
459
+ <h2>New Session</h2>
460
+ <label for="sess-name">Name</label>
461
+ <input type="text" id="sess-name" placeholder="My Session" />
462
+ <label for="sess-shell">Shell / Command</label>
463
+ <input type="text" id="sess-shell" placeholder="/bin/zsh" />
464
+ <label for="sess-cwd">Working Directory</label>
465
+ <div class="cwd-picker">
466
+ <input type="text" id="sess-cwd" placeholder="/Users/dorlugasigal" />
467
+ <button type="button" class="cwd-browse-btn" id="browse-btn" title="Browse folders">
468
+ <svg
469
+ width="18"
470
+ height="18"
471
+ viewBox="0 0 24 24"
472
+ fill="none"
473
+ stroke="currentColor"
474
+ stroke-width="2"
475
+ stroke-linecap="round"
476
+ stroke-linejoin="round"
477
+ >
478
+ <path
479
+ d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
480
+ />
481
+ </svg>
482
+ </button>
483
+ </div>
484
+ <div class="modal-actions">
485
+ <button class="btn-cancel" id="modal-cancel">Cancel</button>
486
+ <button class="btn-create" id="modal-create">Create</button>
487
+ </div>
488
+ </div>
489
+ </div>
490
+
491
+ <!-- Folder browser -->
492
+ <div class="browser-overlay" id="browser-overlay">
493
+ <div class="browser-sheet">
494
+ <div class="browser-header">
495
+ <h3>
496
+ <svg
497
+ width="18"
498
+ height="18"
499
+ viewBox="0 0 24 24"
500
+ fill="none"
501
+ stroke="currentColor"
502
+ stroke-width="2"
503
+ stroke-linecap="round"
504
+ stroke-linejoin="round"
505
+ style="vertical-align: -3px; margin-right: 6px"
506
+ >
507
+ <path
508
+ d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
509
+ /></svg
510
+ >Choose Folder
511
+ </h3>
512
+ <button class="browser-close" id="browser-close">×</button>
513
+ </div>
514
+ <div class="browser-breadcrumb" id="browser-breadcrumb"></div>
515
+ <div class="browser-list" id="browser-list"></div>
516
+ <div class="browser-footer">
517
+ <div class="browser-current-path" id="browser-path">/</div>
518
+ <button class="browser-select-btn" id="browser-select">Select This Folder</button>
519
+ </div>
520
+ </div>
521
+ </div>
522
+
523
+ <script>
524
+ const listEl = document.getElementById('sessions-list');
525
+ const modal = document.getElementById('modal');
526
+
527
+ async function loadSessions() {
528
+ const res = await fetch('/api/sessions');
529
+ const sessions = await res.json();
530
+
531
+ if (sessions.length === 0) {
532
+ listEl.innerHTML = '<div class="empty-state">No active sessions</div>';
533
+ return;
534
+ }
535
+
536
+ listEl.innerHTML = sessions
537
+ .map(
538
+ (s) => `
539
+ <div class="swipe-wrap" data-session-id="${s.id}">
540
+ <div class="swipe-delete">
541
+ <button onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg><span>Delete</span></button>
542
+ </div>
543
+ <div class="session-card" onclick="location.href='/terminal?id=${s.id}'">
544
+ <div class="top">
545
+ <div class="name"><span class="dot"></span>${esc(s.name)}</div>
546
+ <span class="pid">PID ${s.pid}</span>
547
+ </div>
548
+ <div class="details">
549
+ <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> ${esc(s.cwd)}</span>
550
+ <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg> ${esc(s.shell)}</span>
551
+ <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> ${s.clients} connected</span>
552
+ </div>
553
+ <button class="connect-btn">Connect →</button>
554
+ </div>
555
+ </div>
556
+ `,
557
+ )
558
+ .join('');
559
+
560
+ // Attach swipe handlers after rendering
561
+ listEl.querySelectorAll('.swipe-wrap').forEach(initSwipe);
562
+ }
563
+
564
+ function esc(str) {
565
+ const d = document.createElement('div');
566
+ d.textContent = str;
567
+ return d.innerHTML;
568
+ }
569
+
570
+ document.getElementById('new-session-btn').addEventListener('click', () => {
571
+ modal.classList.add('visible');
572
+ });
573
+ document.getElementById('modal-cancel').addEventListener('click', () => {
574
+ modal.classList.remove('visible');
575
+ });
576
+ modal.addEventListener('click', (e) => {
577
+ if (e.target === modal) modal.classList.remove('visible');
578
+ });
579
+
580
+ document.getElementById('modal-create').addEventListener('click', async () => {
581
+ const name = document.getElementById('sess-name').value.trim();
582
+ const shell = document.getElementById('sess-shell').value.trim();
583
+ const cwd = document.getElementById('sess-cwd').value.trim();
584
+
585
+ const body = {};
586
+ if (name) body.name = name;
587
+ if (shell) body.shell = shell;
588
+ if (cwd) body.cwd = cwd;
589
+
590
+ const res = await fetch('/api/sessions', {
591
+ method: 'POST',
592
+ headers: { 'Content-Type': 'application/json' },
593
+ body: JSON.stringify(body),
594
+ });
595
+ const data = await res.json();
596
+ location.href = data.url;
597
+ });
598
+
599
+ // --- Swipe to delete ---
600
+ async function deleteSession(id, e) {
601
+ e.stopPropagation();
602
+ const wrap = document.querySelector(`.swipe-wrap[data-session-id="${id}"]`);
603
+ try {
604
+ await fetch(`/api/sessions/${id}`, { method: 'DELETE' });
605
+ if (wrap) {
606
+ wrap.style.transition = 'opacity 0.25s, max-height 0.3s ease 0.1s';
607
+ wrap.style.opacity = '0';
608
+ wrap.style.maxHeight = wrap.offsetHeight + 'px';
609
+ requestAnimationFrame(() => {
610
+ wrap.style.maxHeight = '0';
611
+ wrap.style.overflow = 'hidden';
612
+ });
613
+ setTimeout(() => loadSessions(), 350);
614
+ } else {
615
+ loadSessions();
616
+ }
617
+ } catch {
618
+ loadSessions();
619
+ }
620
+ }
621
+
622
+ function initSwipe(wrap) {
623
+ const card = wrap.querySelector('.session-card');
624
+ const THRESHOLD = 70;
625
+ let startX = 0,
626
+ currentX = 0,
627
+ swiping = false,
628
+ swiped = false;
629
+
630
+ wrap.addEventListener(
631
+ 'touchstart',
632
+ (e) => {
633
+ startX = e.touches[0].clientX;
634
+ currentX = 0;
635
+ swiping = true;
636
+ card.style.transition = 'none';
637
+ },
638
+ { passive: true },
639
+ );
640
+
641
+ wrap.addEventListener(
642
+ 'touchmove',
643
+ (e) => {
644
+ if (!swiping) return;
645
+ const dx = e.touches[0].clientX - startX;
646
+ currentX = Math.min(0, Math.max(-THRESHOLD, dx));
647
+ card.style.transform = `translateX(${currentX}px)`;
648
+ },
649
+ { passive: true },
650
+ );
651
+
652
+ wrap.addEventListener('touchend', () => {
653
+ swiping = false;
654
+ card.style.transition = 'transform 0.2s ease';
655
+ if (currentX < -THRESHOLD / 2) {
656
+ card.style.transform = `translateX(-${THRESHOLD}px)`;
657
+ swiped = true;
658
+ } else {
659
+ card.style.transform = 'translateX(0)';
660
+ swiped = false;
661
+ }
662
+ });
663
+
664
+ // Tap elsewhere to close
665
+ document.addEventListener(
666
+ 'touchstart',
667
+ (e) => {
668
+ if (swiped && !wrap.contains(e.target)) {
669
+ card.style.transition = 'transform 0.2s ease';
670
+ card.style.transform = 'translateX(0)';
671
+ swiped = false;
672
+ }
673
+ },
674
+ { passive: true },
675
+ );
676
+
677
+ // Block card click when swiped open
678
+ card.addEventListener(
679
+ 'click',
680
+ (e) => {
681
+ if (swiped) {
682
+ e.preventDefault();
683
+ e.stopPropagation();
684
+ }
685
+ },
686
+ true,
687
+ );
688
+ }
689
+
690
+ // --- Folder Browser ---
691
+ const cwdInput = document.getElementById('sess-cwd');
692
+ const browserOverlay = document.getElementById('browser-overlay');
693
+ const browserList = document.getElementById('browser-list');
694
+ const browserBreadcrumb = document.getElementById('browser-breadcrumb');
695
+ const browserPath = document.getElementById('browser-path');
696
+ let currentBrowsePath = '/';
697
+
698
+ document.getElementById('browse-btn').addEventListener('click', () => {
699
+ const initial = cwdInput.value.trim() || '/';
700
+ navigateTo(initial);
701
+ browserOverlay.classList.add('visible');
702
+ });
703
+
704
+ document.getElementById('browser-close').addEventListener('click', () => {
705
+ browserOverlay.classList.remove('visible');
706
+ });
707
+ browserOverlay.addEventListener('click', (e) => {
708
+ if (e.target === browserOverlay) browserOverlay.classList.remove('visible');
709
+ });
710
+
711
+ document.getElementById('browser-select').addEventListener('click', () => {
712
+ cwdInput.value = currentBrowsePath;
713
+ browserOverlay.classList.remove('visible');
714
+ });
715
+
716
+ async function navigateTo(dir) {
717
+ currentBrowsePath = dir;
718
+ browserPath.textContent = dir;
719
+ renderBreadcrumb(dir);
720
+ browserList.innerHTML = '<div class="browser-empty">Loading…</div>';
721
+
722
+ try {
723
+ const res = await fetch(`/api/dirs?q=${encodeURIComponent(dir + '/')}`);
724
+ const data = await res.json();
725
+ if (!data.dirs.length) {
726
+ browserList.innerHTML = '<div class="browser-empty">No subfolders</div>';
727
+ return;
728
+ }
729
+ browserList.innerHTML = data.dirs
730
+ .map((d) => {
731
+ const name = d.split('/').pop();
732
+ return `<div class="folder-item" data-path="${esc(d)}">
733
+ <span class="folder-icon">📁</span>
734
+ <span class="folder-name">${esc(name)}</span>
735
+ <span class="folder-arrow">›</span>
736
+ </div>`;
737
+ })
738
+ .join('');
739
+
740
+ browserList.querySelectorAll('.folder-item').forEach((el) => {
741
+ el.addEventListener('click', () => navigateTo(el.dataset.path));
742
+ });
743
+ browserList.scrollTop = 0;
744
+ } catch {
745
+ browserList.innerHTML = '<div class="browser-empty">Error loading folders</div>';
746
+ }
747
+ }
748
+
749
+ function renderBreadcrumb(dir) {
750
+ const parts = dir.split('/').filter(Boolean);
751
+ let html = `<button class="crumb" data-path="/">/</button>`;
752
+ let accumulated = '';
753
+ parts.forEach((part, i) => {
754
+ accumulated += '/' + part;
755
+ const isCurrent = i === parts.length - 1;
756
+ html += `<span class="crumb-sep">›</span>`;
757
+ html += `<button class="crumb${isCurrent ? ' current' : ''}" data-path="${esc(accumulated)}">${esc(part)}</button>`;
758
+ });
759
+ browserBreadcrumb.innerHTML = html;
760
+ browserBreadcrumb.querySelectorAll('.crumb').forEach((el) => {
761
+ el.addEventListener('click', () => navigateTo(el.dataset.path));
762
+ });
763
+ // Scroll breadcrumb to end
764
+ browserBreadcrumb.scrollLeft = browserBreadcrumb.scrollWidth;
765
+ }
766
+
767
+ // Fetch version
768
+ fetch('/api/version')
769
+ .then((r) => r.json())
770
+ .then((d) => {
771
+ document.getElementById('version').textContent = 'v' + d.version;
772
+ })
773
+ .catch(() => {});
774
+
775
+ loadSessions();
776
+ setInterval(loadSessions, 3000);
777
+ </script>
778
+ </body>
779
+ </html>