claude-remote-cli 0.1.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.
package/public/app.js ADDED
@@ -0,0 +1,706 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ // State
5
+ var activeSessionId = null;
6
+ var ws = null;
7
+ var term = null;
8
+ var fitAddon = null;
9
+
10
+ var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
11
+
12
+ // DOM refs
13
+ var pinGate = document.getElementById('pin-gate');
14
+ var pinInput = document.getElementById('pin-input');
15
+ var pinSubmit = document.getElementById('pin-submit');
16
+ var pinError = document.getElementById('pin-error');
17
+ var mainApp = document.getElementById('main-app');
18
+ var sidebar = document.getElementById('sidebar');
19
+ var sidebarToggle = document.getElementById('sidebar-toggle');
20
+ var sessionList = document.getElementById('session-list');
21
+ var newSessionBtn = document.getElementById('new-session-btn');
22
+ var terminalContainer = document.getElementById('terminal-container');
23
+ var noSessionMsg = document.getElementById('no-session-msg');
24
+ var toolbar = document.getElementById('toolbar');
25
+ var dialog = document.getElementById('new-session-dialog');
26
+ var customPath = document.getElementById('custom-path-input');
27
+ var dialogCancel = document.getElementById('dialog-cancel');
28
+ var dialogStart = document.getElementById('dialog-start');
29
+ var menuBtn = document.getElementById('menu-btn');
30
+ var sidebarOverlay = document.getElementById('sidebar-overlay');
31
+ var sessionTitle = document.getElementById('session-title');
32
+ var sessionFilter = document.getElementById('session-filter');
33
+ var sidebarRootFilter = document.getElementById('sidebar-root-filter');
34
+ var sidebarRepoFilter = document.getElementById('sidebar-repo-filter');
35
+ var dialogRootSelect = document.getElementById('dialog-root-select');
36
+ var dialogRepoSelect = document.getElementById('dialog-repo-select');
37
+
38
+ // Session / worktree / repo state
39
+ var cachedSessions = [];
40
+ var cachedWorktrees = [];
41
+ var allRepos = [];
42
+
43
+ // ── PIN Auth ────────────────────────────────────────────────────────────────
44
+
45
+ function submitPin() {
46
+ var pin = pinInput.value.trim();
47
+ if (!pin) return;
48
+
49
+ fetch('/auth', {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ pin: pin }),
53
+ })
54
+ .then(function (res) {
55
+ if (res.ok) {
56
+ pinGate.hidden = true;
57
+ mainApp.hidden = false;
58
+ initApp();
59
+ } else {
60
+ return res.json().then(function (data) {
61
+ showPinError(data.error || 'Incorrect PIN');
62
+ });
63
+ }
64
+ })
65
+ .catch(function () {
66
+ showPinError('Connection error. Please try again.');
67
+ });
68
+ }
69
+
70
+ function showPinError(msg) {
71
+ pinError.textContent = msg;
72
+ pinError.hidden = false;
73
+ pinInput.value = '';
74
+ pinInput.focus();
75
+ }
76
+
77
+ pinSubmit.addEventListener('click', submitPin);
78
+ pinInput.addEventListener('keydown', function (e) {
79
+ if (e.key === 'Enter') submitPin();
80
+ });
81
+
82
+ // ── App Init ────────────────────────────────────────────────────────────────
83
+
84
+ function initApp() {
85
+ initTerminal();
86
+ loadRepos();
87
+ refreshAll();
88
+ }
89
+
90
+ // ── Terminal ────────────────────────────────────────────────────────────────
91
+
92
+ function initTerminal() {
93
+ term = new Terminal({
94
+ cursorBlink: true,
95
+ fontSize: 14,
96
+ fontFamily: 'Menlo, monospace',
97
+ theme: {
98
+ background: '#1e1e1e',
99
+ foreground: '#d4d4d4',
100
+ cursor: '#d4d4d4',
101
+ },
102
+ });
103
+
104
+ fitAddon = new FitAddon.FitAddon();
105
+ term.loadAddon(fitAddon);
106
+ term.open(terminalContainer);
107
+ fitAddon.fit();
108
+
109
+ term.onData(function (data) {
110
+ if (ws && ws.readyState === WebSocket.OPEN) {
111
+ ws.send(data);
112
+ }
113
+ });
114
+
115
+ var resizeObserver = new ResizeObserver(function () {
116
+ fitAddon.fit();
117
+ sendResize();
118
+ });
119
+ resizeObserver.observe(terminalContainer);
120
+ }
121
+
122
+ function sendResize() {
123
+ if (ws && ws.readyState === WebSocket.OPEN && term) {
124
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
125
+ }
126
+ }
127
+
128
+ // ── WebSocket / Session Connection ──────────────────────────────────────────
129
+
130
+ function connectToSession(sessionId) {
131
+ if (ws) {
132
+ ws.close();
133
+ ws = null;
134
+ }
135
+
136
+ activeSessionId = sessionId;
137
+ noSessionMsg.hidden = true;
138
+ term.clear();
139
+ term.focus();
140
+ closeSidebar();
141
+ updateSessionTitle();
142
+
143
+ var url = wsProtocol + '//' + location.host + '/ws/' + sessionId;
144
+ ws = new WebSocket(url);
145
+
146
+ ws.onopen = function () {
147
+ sendResize();
148
+ };
149
+
150
+ ws.onmessage = function (event) {
151
+ term.write(event.data);
152
+ };
153
+
154
+ ws.onclose = function () {
155
+ term.write('\r\n[Connection closed]\r\n');
156
+ };
157
+
158
+ ws.onerror = function () {
159
+ term.write('\r\n[WebSocket error]\r\n');
160
+ };
161
+
162
+ highlightActiveSession();
163
+ }
164
+
165
+ // ── Sessions & Worktrees ────────────────────────────────────────────────────
166
+
167
+ function refreshAll() {
168
+ Promise.all([
169
+ fetch('/sessions').then(function (res) { return res.json(); }),
170
+ fetch('/worktrees').then(function (res) { return res.json(); }),
171
+ ])
172
+ .then(function (results) {
173
+ cachedSessions = results[0].sessions || results[0] || [];
174
+ cachedWorktrees = results[1] || [];
175
+ populateSidebarFilters();
176
+ renderUnifiedList();
177
+ })
178
+ .catch(function () {});
179
+ }
180
+
181
+ function populateSidebarFilters() {
182
+ var currentRoot = sidebarRootFilter.value;
183
+ var roots = {};
184
+ cachedSessions.forEach(function (s) {
185
+ if (s.root) roots[s.root] = true;
186
+ });
187
+ cachedWorktrees.forEach(function (wt) {
188
+ if (wt.root) roots[wt.root] = true;
189
+ });
190
+
191
+ sidebarRootFilter.innerHTML = '<option value="">All roots</option>';
192
+ Object.keys(roots).sort().forEach(function (root) {
193
+ var opt = document.createElement('option');
194
+ opt.value = root;
195
+ opt.textContent = rootShortName(root);
196
+ sidebarRootFilter.appendChild(opt);
197
+ });
198
+ if (currentRoot && roots[currentRoot]) {
199
+ sidebarRootFilter.value = currentRoot;
200
+ }
201
+
202
+ updateRepoFilter();
203
+ }
204
+
205
+ function updateRepoFilter() {
206
+ var selectedRoot = sidebarRootFilter.value;
207
+ var currentRepo = sidebarRepoFilter.value;
208
+ var repos = {};
209
+
210
+ cachedSessions.forEach(function (s) {
211
+ if (!selectedRoot || s.root === selectedRoot) {
212
+ if (s.repoName) repos[s.repoName] = true;
213
+ }
214
+ });
215
+ cachedWorktrees.forEach(function (wt) {
216
+ if (!selectedRoot || wt.root === selectedRoot) {
217
+ if (wt.repoName) repos[wt.repoName] = true;
218
+ }
219
+ });
220
+
221
+ sidebarRepoFilter.innerHTML = '<option value="">All repos</option>';
222
+ Object.keys(repos).sort().forEach(function (repoName) {
223
+ var opt = document.createElement('option');
224
+ opt.value = repoName;
225
+ opt.textContent = repoName;
226
+ sidebarRepoFilter.appendChild(opt);
227
+ });
228
+ if (currentRepo && repos[currentRepo]) {
229
+ sidebarRepoFilter.value = currentRepo;
230
+ }
231
+ }
232
+
233
+ sidebarRootFilter.addEventListener('change', function () {
234
+ updateRepoFilter();
235
+ renderUnifiedList();
236
+ });
237
+
238
+ sidebarRepoFilter.addEventListener('change', function () {
239
+ renderUnifiedList();
240
+ });
241
+
242
+ sessionFilter.addEventListener('input', function () {
243
+ renderUnifiedList();
244
+ });
245
+
246
+ function rootShortName(path) {
247
+ return path.split('/').filter(Boolean).pop() || path;
248
+ }
249
+
250
+ function renderUnifiedList() {
251
+ var rootFilter = sidebarRootFilter.value;
252
+ var repoFilter = sidebarRepoFilter.value;
253
+ var textFilter = sessionFilter.value.toLowerCase();
254
+
255
+ var filteredSessions = cachedSessions.filter(function (s) {
256
+ if (rootFilter && s.root !== rootFilter) return false;
257
+ if (repoFilter && s.repoName !== repoFilter) return false;
258
+ if (textFilter) {
259
+ var name = (s.displayName || s.repoName || s.worktreeName || s.id).toLowerCase();
260
+ if (name.indexOf(textFilter) === -1) return false;
261
+ }
262
+ return true;
263
+ });
264
+
265
+ var activeWorktreePaths = new Set();
266
+ cachedSessions.forEach(function (s) {
267
+ if (s.repoPath) activeWorktreePaths.add(s.repoPath);
268
+ });
269
+
270
+ var filteredWorktrees = cachedWorktrees.filter(function (wt) {
271
+ if (activeWorktreePaths.has(wt.path)) return false;
272
+ if (rootFilter && wt.root !== rootFilter) return false;
273
+ if (repoFilter && wt.repoName !== repoFilter) return false;
274
+ if (textFilter) {
275
+ var name = (wt.name || '').toLowerCase();
276
+ if (name.indexOf(textFilter) === -1) return false;
277
+ }
278
+ return true;
279
+ });
280
+
281
+ filteredWorktrees.sort(function (a, b) {
282
+ return (a.name || '').localeCompare(b.name || '');
283
+ });
284
+
285
+ sessionList.innerHTML = '';
286
+
287
+ filteredSessions.forEach(function (session) {
288
+ sessionList.appendChild(createActiveSessionLi(session));
289
+ });
290
+
291
+ if (filteredSessions.length > 0 && filteredWorktrees.length > 0) {
292
+ var divider = document.createElement('li');
293
+ divider.className = 'session-divider';
294
+ divider.textContent = 'Available';
295
+ sessionList.appendChild(divider);
296
+ }
297
+
298
+ filteredWorktrees.forEach(function (wt) {
299
+ sessionList.appendChild(createInactiveWorktreeLi(wt));
300
+ });
301
+
302
+ highlightActiveSession();
303
+ }
304
+
305
+ function createActiveSessionLi(session) {
306
+ var li = document.createElement('li');
307
+ li.className = 'active-session';
308
+ li.dataset.sessionId = session.id;
309
+
310
+ var infoDiv = document.createElement('div');
311
+ infoDiv.className = 'session-info';
312
+
313
+ var nameSpan = document.createElement('span');
314
+ nameSpan.className = 'session-name';
315
+ var displayName = session.displayName || session.repoName || session.id;
316
+ nameSpan.textContent = displayName;
317
+ nameSpan.title = displayName;
318
+
319
+ var subSpan = document.createElement('span');
320
+ subSpan.className = 'session-sub';
321
+ subSpan.textContent = (session.root ? rootShortName(session.root) : '') + ' · ' + (session.repoName || '');
322
+
323
+ infoDiv.appendChild(nameSpan);
324
+ infoDiv.appendChild(subSpan);
325
+
326
+ var actionsDiv = document.createElement('div');
327
+ actionsDiv.className = 'session-actions';
328
+
329
+ var renameBtn = document.createElement('button');
330
+ renameBtn.className = 'session-rename-btn';
331
+ renameBtn.setAttribute('aria-label', 'Rename session');
332
+ renameBtn.textContent = '✎';
333
+ renameBtn.addEventListener('click', function (e) {
334
+ e.stopPropagation();
335
+ startRename(li, session);
336
+ });
337
+
338
+ var killBtn = document.createElement('button');
339
+ killBtn.className = 'session-kill';
340
+ killBtn.setAttribute('aria-label', 'Kill session');
341
+ killBtn.textContent = '×';
342
+ killBtn.addEventListener('click', function (e) {
343
+ e.stopPropagation();
344
+ killSession(session.id);
345
+ });
346
+
347
+ actionsDiv.appendChild(renameBtn);
348
+ actionsDiv.appendChild(killBtn);
349
+
350
+ li.appendChild(infoDiv);
351
+ li.appendChild(actionsDiv);
352
+
353
+ li.addEventListener('click', function () {
354
+ connectToSession(session.id);
355
+ });
356
+
357
+ return li;
358
+ }
359
+
360
+ function createInactiveWorktreeLi(wt) {
361
+ var li = document.createElement('li');
362
+ li.className = 'inactive-worktree';
363
+
364
+ var infoDiv = document.createElement('div');
365
+ infoDiv.className = 'session-info';
366
+
367
+ var nameSpan = document.createElement('span');
368
+ nameSpan.className = 'session-name';
369
+ nameSpan.textContent = wt.name;
370
+ nameSpan.title = wt.name;
371
+
372
+ var subSpan = document.createElement('span');
373
+ subSpan.className = 'session-sub';
374
+ subSpan.textContent = (wt.root ? rootShortName(wt.root) : '') + ' · ' + (wt.repoName || '');
375
+
376
+ infoDiv.appendChild(nameSpan);
377
+ infoDiv.appendChild(subSpan);
378
+
379
+ li.appendChild(infoDiv);
380
+
381
+ li.addEventListener('click', function () {
382
+ startSession(wt.repoPath, wt.path);
383
+ });
384
+
385
+ return li;
386
+ }
387
+
388
+ function startRename(li, session) {
389
+ var nameSpan = li.querySelector('.session-name');
390
+ if (!nameSpan) return;
391
+
392
+ var currentName = nameSpan.textContent;
393
+ var input = document.createElement('input');
394
+ input.type = 'text';
395
+ input.className = 'session-rename-input';
396
+ input.value = currentName;
397
+
398
+ nameSpan.replaceWith(input);
399
+ input.focus();
400
+ input.select();
401
+
402
+ var committed = false;
403
+
404
+ function commit() {
405
+ if (committed) return;
406
+ committed = true;
407
+ var newName = input.value.trim();
408
+ if (!newName || newName === currentName) {
409
+ cancel();
410
+ return;
411
+ }
412
+ fetch('/sessions/' + session.id, {
413
+ method: 'PATCH',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify({ displayName: newName }),
416
+ })
417
+ .then(function () { refreshAll(); })
418
+ .catch(function () { cancel(); });
419
+ }
420
+
421
+ function cancel() {
422
+ committed = true;
423
+ input.replaceWith(nameSpan);
424
+ }
425
+
426
+ input.addEventListener('keydown', function (e) {
427
+ if (e.key === 'Enter') { e.preventDefault(); commit(); }
428
+ if (e.key === 'Escape') { e.preventDefault(); cancel(); }
429
+ });
430
+
431
+ input.addEventListener('blur', commit);
432
+ }
433
+
434
+ function killSession(sessionId) {
435
+ fetch('/sessions/' + sessionId, { method: 'DELETE' })
436
+ .then(function () {
437
+ if (sessionId === activeSessionId) {
438
+ if (ws) {
439
+ ws.close();
440
+ ws = null;
441
+ }
442
+ activeSessionId = null;
443
+ term.clear();
444
+ noSessionMsg.hidden = false;
445
+ updateSessionTitle();
446
+ }
447
+ refreshAll();
448
+ })
449
+ .catch(function () {});
450
+ }
451
+
452
+ function highlightActiveSession() {
453
+ var items = sessionList.querySelectorAll('li');
454
+ items.forEach(function (li) {
455
+ if (li.dataset.sessionId === activeSessionId) {
456
+ li.classList.add('active');
457
+ } else {
458
+ li.classList.remove('active');
459
+ }
460
+ });
461
+ }
462
+
463
+ function updateSessionTitle() {
464
+ if (!activeSessionId) {
465
+ sessionTitle.textContent = 'No session';
466
+ return;
467
+ }
468
+ var activeLi = sessionList.querySelector('li.active .session-name');
469
+ sessionTitle.textContent = activeLi ? activeLi.textContent : activeSessionId.slice(0, 8);
470
+ }
471
+
472
+ // ── Repos & New Session Dialog ──────────────────────────────────────────────
473
+
474
+ function loadRepos() {
475
+ fetch('/repos')
476
+ .then(function (res) { return res.json(); })
477
+ .then(function (data) {
478
+ allRepos = data.repos || data || [];
479
+ })
480
+ .catch(function () {});
481
+ }
482
+
483
+ function populateDialogRootSelect() {
484
+ var roots = {};
485
+ allRepos.forEach(function (repo) {
486
+ var root = repo.root || 'Other';
487
+ roots[root] = true;
488
+ });
489
+ dialogRootSelect.innerHTML = '<option value="">Select a root...</option>';
490
+ Object.keys(roots).forEach(function (root) {
491
+ var opt = document.createElement('option');
492
+ opt.value = root;
493
+ opt.textContent = rootShortName(root);
494
+ dialogRootSelect.appendChild(opt);
495
+ });
496
+ }
497
+
498
+ dialogRootSelect.addEventListener('change', function () {
499
+ var root = dialogRootSelect.value;
500
+ dialogRepoSelect.innerHTML = '<option value="">Select a repo...</option>';
501
+
502
+ if (!root) {
503
+ dialogRepoSelect.disabled = true;
504
+ return;
505
+ }
506
+
507
+ var filtered = allRepos.filter(function (r) { return r.root === root; });
508
+ filtered.sort(function (a, b) { return a.name.localeCompare(b.name); });
509
+ filtered.forEach(function (repo) {
510
+ var opt = document.createElement('option');
511
+ opt.value = repo.path;
512
+ opt.textContent = repo.name;
513
+ dialogRepoSelect.appendChild(opt);
514
+ });
515
+ dialogRepoSelect.disabled = false;
516
+ });
517
+
518
+ function startSession(repoPath, worktreePath) {
519
+ var body = {
520
+ repoPath: repoPath,
521
+ repoName: repoPath.split('/').filter(Boolean).pop(),
522
+ };
523
+ if (worktreePath) body.worktreePath = worktreePath;
524
+
525
+ fetch('/sessions', {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify(body),
529
+ })
530
+ .then(function (res) { return res.json(); })
531
+ .then(function (data) {
532
+ if (dialog.open) dialog.close();
533
+ refreshAll();
534
+ if (data.id || data.sessionId) {
535
+ connectToSession(data.id || data.sessionId);
536
+ }
537
+ })
538
+ .catch(function () {});
539
+ }
540
+
541
+ newSessionBtn.addEventListener('click', function () {
542
+ customPath.value = '';
543
+ populateDialogRootSelect();
544
+
545
+ var sidebarRoot = sidebarRootFilter.value;
546
+ if (sidebarRoot) {
547
+ dialogRootSelect.value = sidebarRoot;
548
+ dialogRootSelect.dispatchEvent(new Event('change'));
549
+ var sidebarRepo = sidebarRepoFilter.value;
550
+ if (sidebarRepo) {
551
+ var matchingRepo = allRepos.find(function (r) {
552
+ return r.root === sidebarRoot && r.name === sidebarRepo;
553
+ });
554
+ if (matchingRepo) {
555
+ dialogRepoSelect.value = matchingRepo.path;
556
+ }
557
+ }
558
+ } else {
559
+ dialogRepoSelect.innerHTML = '<option value="">Select a repo...</option>';
560
+ dialogRepoSelect.disabled = true;
561
+ }
562
+
563
+ dialog.showModal();
564
+ });
565
+
566
+ dialogStart.addEventListener('click', function () {
567
+ var path = customPath.value.trim() || dialogRepoSelect.value;
568
+ if (!path) return;
569
+ startSession(path, null);
570
+ });
571
+
572
+ dialogCancel.addEventListener('click', function () {
573
+ dialog.close();
574
+ });
575
+
576
+ // ── Sidebar Toggle ──────────────────────────────────────────────────────────
577
+
578
+ function openSidebar() {
579
+ sidebar.classList.add('open');
580
+ sidebarOverlay.classList.add('visible');
581
+ }
582
+
583
+ function closeSidebar() {
584
+ sidebar.classList.remove('open');
585
+ sidebarOverlay.classList.remove('visible');
586
+ }
587
+
588
+ menuBtn.addEventListener('click', openSidebar);
589
+ sidebarToggle.addEventListener('click', closeSidebar);
590
+ sidebarOverlay.addEventListener('click', closeSidebar);
591
+
592
+ // ── Settings Dialog ────────────────────────────────────────────────────────
593
+
594
+ var settingsBtn = document.getElementById('settings-btn');
595
+ var settingsDialog = document.getElementById('settings-dialog');
596
+ var settingsRootsList = document.getElementById('settings-roots-list');
597
+ var addRootPath = document.getElementById('add-root-path');
598
+ var addRootBtn = document.getElementById('add-root-btn');
599
+ var settingsClose = document.getElementById('settings-close');
600
+
601
+ function renderRoots(roots) {
602
+ settingsRootsList.innerHTML = '';
603
+ roots.forEach(function (rootPath) {
604
+ var div = document.createElement('div');
605
+ div.className = 'settings-repo-item';
606
+
607
+ var pathSpan = document.createElement('span');
608
+ pathSpan.className = 'repo-path';
609
+ pathSpan.textContent = rootPath;
610
+
611
+ var removeBtn = document.createElement('button');
612
+ removeBtn.className = 'remove-repo';
613
+ removeBtn.textContent = '×';
614
+ removeBtn.addEventListener('click', function () {
615
+ fetch('/roots', {
616
+ method: 'DELETE',
617
+ headers: { 'Content-Type': 'application/json' },
618
+ body: JSON.stringify({ path: rootPath }),
619
+ })
620
+ .then(function (res) { return res.json(); })
621
+ .then(function (updated) {
622
+ renderRoots(updated);
623
+ loadRepos();
624
+ })
625
+ .catch(function () {});
626
+ });
627
+
628
+ div.appendChild(pathSpan);
629
+ div.appendChild(removeBtn);
630
+ settingsRootsList.appendChild(div);
631
+ });
632
+ }
633
+
634
+ function openSettings() {
635
+ fetch('/roots')
636
+ .then(function (res) { return res.json(); })
637
+ .then(function (roots) {
638
+ renderRoots(roots);
639
+ settingsDialog.showModal();
640
+ })
641
+ .catch(function () {});
642
+ }
643
+
644
+ settingsBtn.addEventListener('click', openSettings);
645
+
646
+ addRootBtn.addEventListener('click', function () {
647
+ var rootPath = addRootPath.value.trim();
648
+ if (!rootPath) return;
649
+
650
+ fetch('/roots', {
651
+ method: 'POST',
652
+ headers: { 'Content-Type': 'application/json' },
653
+ body: JSON.stringify({ path: rootPath }),
654
+ })
655
+ .then(function (res) { return res.json(); })
656
+ .then(function (updated) {
657
+ renderRoots(updated);
658
+ addRootPath.value = '';
659
+ loadRepos();
660
+ })
661
+ .catch(function () {});
662
+ });
663
+
664
+ addRootPath.addEventListener('keydown', function (e) {
665
+ if (e.key === 'Enter') addRootBtn.click();
666
+ });
667
+
668
+ settingsClose.addEventListener('click', function () {
669
+ settingsDialog.close();
670
+ loadRepos();
671
+ });
672
+
673
+ // ── Touch Toolbar ───────────────────────────────────────────────────────────
674
+
675
+ toolbar.addEventListener('click', function (e) {
676
+ var btn = e.target.closest('button');
677
+ if (!btn) return;
678
+
679
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
680
+
681
+ var text = btn.dataset.text;
682
+ var key = btn.dataset.key;
683
+
684
+ if (text !== undefined) {
685
+ ws.send(text);
686
+ } else if (key !== undefined) {
687
+ ws.send(key);
688
+ }
689
+ });
690
+
691
+ // ── Auto-auth Check ─────────────────────────────────────────────────────────
692
+
693
+ fetch('/sessions')
694
+ .then(function (res) {
695
+ if (res.ok) {
696
+ // Already authenticated — skip PIN gate
697
+ pinGate.hidden = true;
698
+ mainApp.hidden = false;
699
+ initApp();
700
+ }
701
+ // If not ok (401/403), stay on PIN gate — no action needed
702
+ })
703
+ .catch(function () {
704
+ // Network error — stay on PIN gate
705
+ });
706
+ })();