session-collab-mcp 0.3.0 → 0.4.3

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.
@@ -1,1181 +0,0 @@
1
- // Frontend App - Session Collab MCP Dashboard
2
- // Security: All user-provided content is escaped via escapeHtml() before DOM insertion
3
-
4
- export function generateAppHtml(origin: string): string {
5
- return `<!DOCTYPE html>
6
- <html lang="en">
7
- <head>
8
- <meta charset="UTF-8">
9
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
- <title>Session Collab MCP - Dashboard</title>
11
- <style>
12
- * { box-sizing: border-box; margin: 0; padding: 0; }
13
-
14
- :root {
15
- --bg-primary: #0f0f1a;
16
- --bg-secondary: #1a1a2e;
17
- --bg-card: #16213e;
18
- --bg-input: #0d1b2a;
19
- --text-primary: #e4e4e7;
20
- --text-secondary: #a1a1aa;
21
- --text-muted: #71717a;
22
- --accent-blue: #60a5fa;
23
- --accent-purple: #a78bfa;
24
- --accent-green: #22c55e;
25
- --accent-red: #ef4444;
26
- --accent-yellow: #eab308;
27
- --border: #2d2d3a;
28
- }
29
-
30
- body {
31
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
32
- background: var(--bg-primary);
33
- color: var(--text-primary);
34
- min-height: 100vh;
35
- line-height: 1.6;
36
- }
37
-
38
- .app { display: flex; flex-direction: column; min-height: 100vh; }
39
-
40
- header {
41
- background: var(--bg-secondary);
42
- border-bottom: 1px solid var(--border);
43
- padding: 1rem 2rem;
44
- display: flex;
45
- justify-content: space-between;
46
- align-items: center;
47
- }
48
-
49
- .logo {
50
- font-size: 1.25rem;
51
- font-weight: 700;
52
- background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
53
- -webkit-background-clip: text;
54
- -webkit-text-fill-color: transparent;
55
- }
56
-
57
- .user-info { display: flex; align-items: center; gap: 1rem; }
58
- .user-email { color: var(--text-secondary); font-size: 0.875rem; }
59
-
60
- main {
61
- flex: 1;
62
- padding: 2rem;
63
- max-width: 1200px;
64
- margin: 0 auto;
65
- width: 100%;
66
- }
67
-
68
- .auth-container { max-width: 400px; margin: 4rem auto; }
69
-
70
- .auth-card {
71
- background: var(--bg-card);
72
- border-radius: 12px;
73
- padding: 2rem;
74
- border: 1px solid var(--border);
75
- }
76
-
77
- .auth-tabs {
78
- display: flex;
79
- margin-bottom: 1.5rem;
80
- border-bottom: 1px solid var(--border);
81
- }
82
-
83
- .auth-tab {
84
- flex: 1;
85
- padding: 0.75rem;
86
- background: none;
87
- border: none;
88
- color: var(--text-secondary);
89
- cursor: pointer;
90
- font-size: 1rem;
91
- }
92
-
93
- .auth-tab.active {
94
- color: var(--accent-blue);
95
- border-bottom: 2px solid var(--accent-blue);
96
- margin-bottom: -1px;
97
- }
98
-
99
- .form-group { margin-bottom: 1rem; }
100
-
101
- label {
102
- display: block;
103
- margin-bottom: 0.5rem;
104
- color: var(--text-secondary);
105
- font-size: 0.875rem;
106
- }
107
-
108
- input[type="text"],
109
- input[type="email"],
110
- input[type="password"],
111
- input[type="number"] {
112
- width: 100%;
113
- padding: 0.75rem 1rem;
114
- background: var(--bg-input);
115
- border: 1px solid var(--border);
116
- border-radius: 8px;
117
- color: var(--text-primary);
118
- font-size: 1rem;
119
- }
120
-
121
- input:focus { outline: none; border-color: var(--accent-blue); }
122
-
123
- .btn {
124
- padding: 0.75rem 1.5rem;
125
- border-radius: 8px;
126
- font-size: 0.875rem;
127
- font-weight: 500;
128
- cursor: pointer;
129
- border: none;
130
- }
131
-
132
- .btn-primary {
133
- background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
134
- color: white;
135
- width: 100%;
136
- }
137
-
138
- .btn-secondary {
139
- background: var(--bg-secondary);
140
- color: var(--text-primary);
141
- border: 1px solid var(--border);
142
- }
143
-
144
- .btn-danger { background: var(--accent-red); color: white; }
145
- .btn-sm { padding: 0.5rem 1rem; font-size: 0.75rem; }
146
-
147
- .dashboard { display: none; }
148
- .dashboard.active { display: block; }
149
- .auth-container.hidden { display: none; }
150
-
151
- .dashboard-grid {
152
- display: grid;
153
- grid-template-columns: 300px 1fr;
154
- gap: 1.5rem;
155
- align-items: start;
156
- }
157
-
158
- @media (max-width: 768px) {
159
- .dashboard-grid {
160
- grid-template-columns: 1fr;
161
- }
162
- }
163
-
164
- .card {
165
- background: var(--bg-card);
166
- border-radius: 12px;
167
- padding: 1.5rem;
168
- border: 1px solid var(--border);
169
- }
170
-
171
- .card-header {
172
- display: flex;
173
- justify-content: space-between;
174
- align-items: center;
175
- margin-bottom: 1rem;
176
- }
177
-
178
- .card-title { font-size: 1.125rem; font-weight: 600; }
179
-
180
- .token-list, .session-list {
181
- display: flex;
182
- flex-direction: column;
183
- gap: 0.75rem;
184
- }
185
-
186
- .session-list {
187
- max-height: 70vh;
188
- overflow-y: auto;
189
- padding-right: 0.5rem;
190
- }
191
-
192
- .session-list::-webkit-scrollbar {
193
- width: 6px;
194
- }
195
-
196
- .session-list::-webkit-scrollbar-track {
197
- background: var(--bg-input);
198
- border-radius: 3px;
199
- }
200
-
201
- .session-list::-webkit-scrollbar-thumb {
202
- background: var(--border);
203
- border-radius: 3px;
204
- }
205
-
206
- .session-list::-webkit-scrollbar-thumb:hover {
207
- background: var(--text-muted);
208
- }
209
-
210
- .token-item, .session-item {
211
- display: flex;
212
- justify-content: space-between;
213
- align-items: center;
214
- padding: 1rem;
215
- background: var(--bg-secondary);
216
- border-radius: 8px;
217
- border: 1px solid var(--border);
218
- }
219
-
220
- .token-info h4, .session-info h4 {
221
- font-size: 0.875rem;
222
- font-weight: 500;
223
- margin-bottom: 0.25rem;
224
- }
225
-
226
- .token-meta, .session-meta {
227
- font-size: 0.75rem;
228
- color: var(--text-muted);
229
- }
230
-
231
- .token-prefix {
232
- font-family: monospace;
233
- background: var(--bg-input);
234
- padding: 0.125rem 0.5rem;
235
- border-radius: 4px;
236
- }
237
-
238
- .status-badge {
239
- padding: 0.25rem 0.5rem;
240
- border-radius: 9999px;
241
- font-size: 0.625rem;
242
- font-weight: 600;
243
- text-transform: uppercase;
244
- }
245
-
246
- .status-active { background: rgba(34, 197, 94, 0.2); color: var(--accent-green); }
247
- .status-inactive { background: rgba(234, 179, 8, 0.2); color: var(--accent-yellow); }
248
-
249
- .alert {
250
- padding: 0.75rem 1rem;
251
- border-radius: 8px;
252
- margin-bottom: 1rem;
253
- font-size: 0.875rem;
254
- }
255
-
256
- .alert-error {
257
- background: rgba(239, 68, 68, 0.2);
258
- color: var(--accent-red);
259
- }
260
-
261
- .alert-success {
262
- background: rgba(34, 197, 94, 0.2);
263
- color: var(--accent-green);
264
- }
265
-
266
- .modal-overlay {
267
- position: fixed;
268
- top: 0; left: 0; right: 0; bottom: 0;
269
- background: rgba(0, 0, 0, 0.7);
270
- display: none;
271
- align-items: center;
272
- justify-content: center;
273
- z-index: 1000;
274
- }
275
-
276
- .modal-overlay.active { display: flex; }
277
-
278
- .modal {
279
- background: var(--bg-card);
280
- border-radius: 12px;
281
- padding: 2rem;
282
- max-width: 500px;
283
- width: 90%;
284
- border: 1px solid var(--border);
285
- }
286
-
287
- .modal-header {
288
- display: flex;
289
- justify-content: space-between;
290
- align-items: center;
291
- margin-bottom: 1.5rem;
292
- }
293
-
294
- .modal-title { font-size: 1.25rem; font-weight: 600; }
295
-
296
- .modal-close {
297
- background: none;
298
- border: none;
299
- color: var(--text-muted);
300
- cursor: pointer;
301
- font-size: 1.5rem;
302
- }
303
-
304
- .token-display {
305
- background: var(--bg-input);
306
- padding: 1rem;
307
- border-radius: 8px;
308
- font-family: monospace;
309
- font-size: 0.875rem;
310
- word-break: break-all;
311
- margin: 1rem 0;
312
- border: 1px solid var(--accent-green);
313
- }
314
-
315
- .token-warning {
316
- color: var(--accent-yellow);
317
- font-size: 0.75rem;
318
- }
319
-
320
- .empty-state {
321
- text-align: center;
322
- padding: 2rem;
323
- color: var(--text-muted);
324
- }
325
-
326
- .empty-state p { margin-bottom: 1rem; }
327
-
328
- .setup-guide {
329
- grid-column: 1 / -1;
330
- }
331
-
332
- .setup-guide .collapsible-content {
333
- max-height: 0;
334
- overflow: hidden;
335
- }
336
-
337
- .setup-guide .collapsible-content.expanded {
338
- max-height: 800px;
339
- overflow-y: auto;
340
- }
341
-
342
- .setup-steps {
343
- display: flex;
344
- flex-direction: column;
345
- gap: 1rem;
346
- }
347
-
348
- .setup-step {
349
- display: flex;
350
- gap: 1rem;
351
- padding: 1rem;
352
- background: var(--bg-secondary);
353
- border-radius: 8px;
354
- border: 1px solid var(--border);
355
- }
356
-
357
- .step-number {
358
- width: 28px;
359
- height: 28px;
360
- background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
361
- border-radius: 50%;
362
- display: flex;
363
- align-items: center;
364
- justify-content: center;
365
- font-weight: 600;
366
- font-size: 0.875rem;
367
- flex-shrink: 0;
368
- }
369
-
370
- .step-content h4 {
371
- font-size: 0.9375rem;
372
- font-weight: 600;
373
- margin-bottom: 0.5rem;
374
- }
375
-
376
- .step-content p {
377
- font-size: 0.8125rem;
378
- color: var(--text-secondary);
379
- margin-bottom: 0.5rem;
380
- }
381
-
382
- .code-block {
383
- background: var(--bg-input);
384
- padding: 0.75rem 1rem;
385
- border-radius: 6px;
386
- font-family: monospace;
387
- font-size: 0.75rem;
388
- overflow-x: auto;
389
- white-space: pre;
390
- color: var(--accent-green);
391
- border: 1px solid var(--border);
392
- }
393
-
394
- .copy-btn {
395
- background: var(--bg-card);
396
- border: 1px solid var(--border);
397
- color: var(--text-secondary);
398
- padding: 0.25rem 0.5rem;
399
- border-radius: 4px;
400
- font-size: 0.625rem;
401
- cursor: pointer;
402
- margin-left: 0.5rem;
403
- }
404
-
405
- .copy-btn:hover {
406
- background: var(--bg-secondary);
407
- color: var(--text-primary);
408
- }
409
-
410
- .collapsible-header {
411
- display: flex;
412
- justify-content: space-between;
413
- align-items: center;
414
- cursor: pointer;
415
- }
416
-
417
- .collapsible-content {
418
- max-height: 0;
419
- overflow: hidden;
420
- transition: max-height 0.3s ease;
421
- }
422
-
423
- .collapsible-content.expanded {
424
- max-height: 2000px;
425
- }
426
-
427
- .expand-icon {
428
- transition: transform 0.3s ease;
429
- }
430
-
431
- .expand-icon.rotated {
432
- transform: rotate(180deg);
433
- }
434
-
435
- .session-item {
436
- flex-direction: column;
437
- align-items: stretch;
438
- gap: 0.75rem;
439
- }
440
-
441
- .session-header {
442
- display: flex;
443
- justify-content: space-between;
444
- align-items: center;
445
- }
446
-
447
- .current-task {
448
- display: flex;
449
- align-items: center;
450
- gap: 0.5rem;
451
- padding: 0.5rem 0.75rem;
452
- background: rgba(96, 165, 250, 0.1);
453
- border-radius: 6px;
454
- border-left: 3px solid var(--accent-blue);
455
- font-size: 0.8125rem;
456
- color: var(--text-primary);
457
- }
458
-
459
- .current-task-label {
460
- color: var(--accent-blue);
461
- font-weight: 500;
462
- font-size: 0.75rem;
463
- }
464
-
465
- .session-todos {
466
- display: flex;
467
- flex-direction: column;
468
- gap: 0.375rem;
469
- padding-top: 0.5rem;
470
- border-top: 1px solid var(--border);
471
- }
472
-
473
- .todo-item {
474
- display: flex;
475
- align-items: center;
476
- gap: 0.5rem;
477
- padding: 0.375rem 0.5rem;
478
- background: var(--bg-input);
479
- border-radius: 4px;
480
- font-size: 0.75rem;
481
- }
482
-
483
- .todo-status {
484
- width: 8px;
485
- height: 8px;
486
- border-radius: 50%;
487
- flex-shrink: 0;
488
- }
489
-
490
- .todo-status.pending { background: var(--text-muted); }
491
- .todo-status.in_progress {
492
- background: var(--accent-blue);
493
- box-shadow: 0 0 6px var(--accent-blue);
494
- animation: pulse 1.5s ease-in-out infinite;
495
- }
496
- .todo-status.completed { background: var(--accent-green); }
497
-
498
- @keyframes pulse {
499
- 0%, 100% { opacity: 1; }
500
- 50% { opacity: 0.5; }
501
- }
502
-
503
- .todo-order {
504
- color: var(--text-muted);
505
- font-size: 0.625rem;
506
- font-weight: 600;
507
- min-width: 1rem;
508
- }
509
-
510
- .todo-content {
511
- flex: 1;
512
- color: var(--text-secondary);
513
- }
514
-
515
- .todo-item.in_progress .todo-content {
516
- color: var(--accent-blue);
517
- font-weight: 500;
518
- }
519
-
520
- .todo-item.completed .todo-content {
521
- color: var(--text-muted);
522
- text-decoration: line-through;
523
- }
524
-
525
- .loading { text-align: center; padding: 2rem; }
526
-
527
- .spinner {
528
- display: inline-block;
529
- width: 24px;
530
- height: 24px;
531
- border: 2px solid var(--border);
532
- border-top-color: var(--accent-blue);
533
- border-radius: 50%;
534
- animation: spin 1s linear infinite;
535
- }
536
-
537
- @keyframes spin { to { transform: rotate(360deg); } }
538
-
539
- @media (max-width: 640px) {
540
- header { padding: 1rem; }
541
- main { padding: 1rem; }
542
- .dashboard-grid { grid-template-columns: 1fr; }
543
- }
544
- </style>
545
- </head>
546
- <body>
547
- <div class="app">
548
- <header>
549
- <div class="logo">Session Collab MCP</div>
550
- <div class="user-info" id="userInfo" style="display: none;">
551
- <span class="user-email" id="userEmail"></span>
552
- <button class="btn btn-secondary btn-sm" id="logoutBtn">Logout</button>
553
- </div>
554
- </header>
555
-
556
- <main>
557
- <div class="auth-container" id="authContainer">
558
- <div class="auth-card">
559
- <div class="auth-tabs">
560
- <button class="auth-tab active" data-tab="login" id="loginTab">Login</button>
561
- <button class="auth-tab" data-tab="register" id="registerTab">Register</button>
562
- </div>
563
-
564
- <div id="authAlert"></div>
565
-
566
- <form id="loginForm">
567
- <div class="form-group">
568
- <label for="loginEmail">Email</label>
569
- <input type="email" id="loginEmail" placeholder="you@example.com" required>
570
- </div>
571
- <div class="form-group">
572
- <label for="loginPassword">Password</label>
573
- <input type="password" id="loginPassword" placeholder="Your password" required>
574
- </div>
575
- <button type="submit" class="btn btn-primary">Login</button>
576
- </form>
577
-
578
- <form id="registerForm" style="display: none;">
579
- <div class="form-group">
580
- <label for="registerName">Display Name</label>
581
- <input type="text" id="registerName" placeholder="Your name">
582
- </div>
583
- <div class="form-group">
584
- <label for="registerEmail">Email</label>
585
- <input type="email" id="registerEmail" placeholder="you@example.com" required>
586
- </div>
587
- <div class="form-group">
588
- <label for="registerPassword">Password</label>
589
- <input type="password" id="registerPassword" placeholder="Min 8 chars" required>
590
- </div>
591
- <button type="submit" class="btn btn-primary">Create Account</button>
592
- </form>
593
- </div>
594
- </div>
595
-
596
- <div class="dashboard" id="dashboard">
597
- <div class="dashboard-grid">
598
- <div class="card">
599
- <div class="card-header">
600
- <h3 class="card-title">API Tokens</h3>
601
- <button class="btn btn-secondary btn-sm" id="newTokenBtn">+ New Token</button>
602
- </div>
603
- <div id="tokenList" class="token-list"></div>
604
- </div>
605
-
606
- <div class="card">
607
- <div class="card-header">
608
- <h3 class="card-title">Active Sessions</h3>
609
- <button class="btn btn-secondary btn-sm" id="refreshSessionsBtn">Refresh</button>
610
- </div>
611
- <div id="sessionList" class="session-list"></div>
612
- </div>
613
-
614
- <div class="card setup-guide">
615
- <div class="card-header collapsible-header" id="setupGuideHeader">
616
- <h3 class="card-title">Setup Guide</h3>
617
- <span class="expand-icon" id="expandIcon">&#9660;</span>
618
- </div>
619
- <div class="collapsible-content" id="setupGuideContent">
620
- <div class="setup-steps">
621
- <div class="setup-step">
622
- <div class="step-number">1</div>
623
- <div class="step-content">
624
- <h4>Save API Token</h4>
625
- <p>Copy your token and save it to ~/.claude/.env</p>
626
- <div class="code-block" id="envCode">MCP_TOKEN="your-token-here"</div>
627
- </div>
628
- </div>
629
-
630
- <div class="setup-step">
631
- <div class="step-number">2</div>
632
- <div class="step-content">
633
- <h4>Create Hook Scripts</h4>
634
- <p>Create .claude/hooks/ directory in your project and add these scripts:</p>
635
- <div class="code-block">mkdir -p .claude/hooks</div>
636
- </div>
637
- </div>
638
-
639
- <div class="setup-step">
640
- <div class="step-number">3</div>
641
- <div class="step-content">
642
- <h4>Configure .claude/settings.json</h4>
643
- <p>Add hooks configuration to your project:</p>
644
- <div class="code-block" id="settingsCode">{
645
- "hooks": {
646
- "SessionStart": [{
647
- "matcher": "",
648
- "hooks": [{
649
- "type": "command",
650
- "command": "bash .claude/hooks/session-start.sh my-session"
651
- }]
652
- }],
653
- "PreToolUse": [{
654
- "matcher": "Edit|Write",
655
- "hooks": [{
656
- "type": "command",
657
- "command": "bash .claude/hooks/check-claims.sh"
658
- }]
659
- }],
660
- "PostToolUse": [{
661
- "matcher": "TodoWrite",
662
- "hooks": [{
663
- "type": "command",
664
- "command": "bash .claude/hooks/todo-sync.sh"
665
- }]
666
- }]
667
- }
668
- }</div>
669
- <button class="btn btn-secondary btn-sm" id="copySettingsBtn" style="margin-top: 0.5rem;">Copy JSON</button>
670
- </div>
671
- </div>
672
-
673
- <div class="setup-step">
674
- <div class="step-number">4</div>
675
- <div class="step-content">
676
- <h4>Download Hook Scripts</h4>
677
- <p>Get the hook scripts from the repository or create them manually:</p>
678
- <div class="code-block"># session-start.sh - Register session on new conversation
679
- # check-claims.sh - Check file conflicts before editing
680
- # todo-sync.sh - Sync todo list after updates
681
-
682
- # See CLAUDE.md in the repo for full script contents</div>
683
- </div>
684
- </div>
685
-
686
- <div class="setup-step">
687
- <div class="step-number">5</div>
688
- <div class="step-content">
689
- <h4>Restart Claude Code</h4>
690
- <p>Start a new conversation to activate the hooks. You should see your session appear above!</p>
691
- </div>
692
- </div>
693
- </div>
694
- </div>
695
- </div>
696
- </div>
697
- </div>
698
- </main>
699
- </div>
700
-
701
- <div class="modal-overlay" id="createTokenModal">
702
- <div class="modal">
703
- <div class="modal-header">
704
- <h3 class="modal-title">Create API Token</h3>
705
- <button class="modal-close" id="closeCreateModal">&times;</button>
706
- </div>
707
- <form id="createTokenForm">
708
- <div class="form-group">
709
- <label for="tokenName">Token Name</label>
710
- <input type="text" id="tokenName" placeholder="e.g., Claude Code" required>
711
- </div>
712
- <div class="form-group">
713
- <label for="tokenExpiry">Expires In (days)</label>
714
- <input type="number" id="tokenExpiry" placeholder="Leave empty for no expiry" min="1" max="365">
715
- </div>
716
- <button type="submit" class="btn btn-primary">Create Token</button>
717
- </form>
718
- </div>
719
- </div>
720
-
721
- <div class="modal-overlay" id="tokenCreatedModal">
722
- <div class="modal">
723
- <div class="modal-header">
724
- <h3 class="modal-title">Token Created</h3>
725
- <button class="modal-close" id="closeTokenModal">&times;</button>
726
- </div>
727
- <p>Copy your token now - it won't be shown again!</p>
728
- <div class="token-display" id="newTokenValue"></div>
729
- <p class="token-warning">Store this token securely.</p>
730
- <button class="btn btn-primary" id="copyTokenBtn">Copy to Clipboard</button>
731
- </div>
732
- </div>
733
-
734
- <script>
735
- (function() {
736
- const API_BASE = '${origin}';
737
- let accessToken = localStorage.getItem('accessToken');
738
- let refreshToken = localStorage.getItem('refreshToken');
739
- let currentUser = null;
740
-
741
- // DOM elements
742
- const $ = (id) => document.getElementById(id);
743
-
744
- // Safe text content setter
745
- function setText(el, text) {
746
- if (el) el.textContent = text || '';
747
- }
748
-
749
- // Safe element creation
750
- function createEl(tag, className, text) {
751
- const el = document.createElement(tag);
752
- if (className) el.className = className;
753
- if (text) el.textContent = text;
754
- return el;
755
- }
756
-
757
- // Initialize
758
- document.addEventListener('DOMContentLoaded', init);
759
-
760
- function init() {
761
- // Event listeners
762
- $('loginTab').addEventListener('click', () => switchTab('login'));
763
- $('registerTab').addEventListener('click', () => switchTab('register'));
764
- $('loginForm').addEventListener('submit', handleLogin);
765
- $('registerForm').addEventListener('submit', handleRegister);
766
- $('logoutBtn').addEventListener('click', logout);
767
- $('newTokenBtn').addEventListener('click', openCreateTokenModal);
768
- $('refreshSessionsBtn').addEventListener('click', loadSessions);
769
- $('closeCreateModal').addEventListener('click', () => closeModal('createTokenModal'));
770
- $('closeTokenModal').addEventListener('click', () => closeModal('tokenCreatedModal'));
771
- $('createTokenForm').addEventListener('submit', handleCreateToken);
772
- $('copyTokenBtn').addEventListener('click', copyToken);
773
- $('setupGuideHeader').addEventListener('click', toggleSetupGuide);
774
- $('copySettingsBtn').addEventListener('click', copySettings);
775
-
776
- if (accessToken) checkAuth();
777
- }
778
-
779
- function switchTab(tab) {
780
- $('loginTab').classList.toggle('active', tab === 'login');
781
- $('registerTab').classList.toggle('active', tab === 'register');
782
- $('loginForm').style.display = tab === 'login' ? 'block' : 'none';
783
- $('registerForm').style.display = tab === 'register' ? 'block' : 'none';
784
- $('authAlert').textContent = '';
785
- }
786
-
787
- function showAlert(type, message) {
788
- const alert = $('authAlert');
789
- alert.className = 'alert alert-' + type;
790
- alert.textContent = message;
791
- }
792
-
793
- async function handleLogin(e) {
794
- e.preventDefault();
795
- const email = $('loginEmail').value;
796
- const password = $('loginPassword').value;
797
-
798
- try {
799
- const res = await fetch(API_BASE + '/auth/login', {
800
- method: 'POST',
801
- headers: { 'Content-Type': 'application/json' },
802
- body: JSON.stringify({ email, password })
803
- });
804
-
805
- const data = await res.json();
806
- if (!res.ok) {
807
- showAlert('error', data.error || 'Login failed');
808
- return;
809
- }
810
-
811
- saveAuth(data);
812
- showDashboard();
813
- } catch (err) {
814
- showAlert('error', 'Network error');
815
- }
816
- }
817
-
818
- async function handleRegister(e) {
819
- e.preventDefault();
820
- const display_name = $('registerName').value;
821
- const email = $('registerEmail').value;
822
- const password = $('registerPassword').value;
823
-
824
- try {
825
- const res = await fetch(API_BASE + '/auth/register', {
826
- method: 'POST',
827
- headers: { 'Content-Type': 'application/json' },
828
- body: JSON.stringify({ email, password, display_name })
829
- });
830
-
831
- const data = await res.json();
832
- if (!res.ok) {
833
- const msg = data.details ? data.details.map(d => d.message).join(', ') : data.error;
834
- showAlert('error', msg || 'Registration failed');
835
- return;
836
- }
837
-
838
- saveAuth(data);
839
- showDashboard();
840
- } catch (err) {
841
- showAlert('error', 'Network error');
842
- }
843
- }
844
-
845
- function saveAuth(data) {
846
- accessToken = data.access_token;
847
- refreshToken = data.refresh_token;
848
- currentUser = data.user;
849
- localStorage.setItem('accessToken', accessToken);
850
- localStorage.setItem('refreshToken', refreshToken);
851
- }
852
-
853
- async function checkAuth() {
854
- try {
855
- const res = await fetch(API_BASE + '/auth/me', {
856
- headers: { 'Authorization': 'Bearer ' + accessToken }
857
- });
858
-
859
- if (res.ok) {
860
- currentUser = await res.json();
861
- showDashboard();
862
- } else if (res.status === 401 && refreshToken) {
863
- await doRefresh();
864
- } else {
865
- logout();
866
- }
867
- } catch (err) {
868
- logout();
869
- }
870
- }
871
-
872
- async function doRefresh() {
873
- try {
874
- const res = await fetch(API_BASE + '/auth/refresh', {
875
- method: 'POST',
876
- headers: { 'Content-Type': 'application/json' },
877
- body: JSON.stringify({ refresh_token: refreshToken })
878
- });
879
-
880
- if (res.ok) {
881
- const data = await res.json();
882
- saveAuth(data);
883
- showDashboard();
884
- } else {
885
- logout();
886
- }
887
- } catch (err) {
888
- logout();
889
- }
890
- }
891
-
892
- function logout() {
893
- accessToken = null;
894
- refreshToken = null;
895
- currentUser = null;
896
- localStorage.removeItem('accessToken');
897
- localStorage.removeItem('refreshToken');
898
- showAuth();
899
- }
900
-
901
- function showAuth() {
902
- $('authContainer').classList.remove('hidden');
903
- $('dashboard').classList.remove('active');
904
- $('userInfo').style.display = 'none';
905
- }
906
-
907
- function showDashboard() {
908
- $('authContainer').classList.add('hidden');
909
- $('dashboard').classList.add('active');
910
- $('userInfo').style.display = 'flex';
911
- setText($('userEmail'), currentUser?.email);
912
- loadTokens();
913
- loadSessions();
914
- }
915
-
916
- async function loadTokens() {
917
- const container = $('tokenList');
918
- container.textContent = '';
919
- const loading = createEl('div', 'loading');
920
- loading.appendChild(createEl('div', 'spinner'));
921
- container.appendChild(loading);
922
-
923
- try {
924
- const res = await fetch(API_BASE + '/tokens', {
925
- headers: { 'Authorization': 'Bearer ' + accessToken }
926
- });
927
-
928
- if (!res.ok) throw new Error('Failed');
929
- const data = await res.json();
930
- container.textContent = '';
931
-
932
- if (data.tokens.length === 0) {
933
- const empty = createEl('div', 'empty-state');
934
- empty.appendChild(createEl('p', '', 'No API tokens yet'));
935
- const btn = createEl('button', 'btn btn-secondary btn-sm', 'Create your first token');
936
- btn.addEventListener('click', openCreateTokenModal);
937
- empty.appendChild(btn);
938
- container.appendChild(empty);
939
- return;
940
- }
941
-
942
- data.tokens.forEach(token => {
943
- const item = createEl('div', 'token-item');
944
-
945
- const info = createEl('div', 'token-info');
946
- info.appendChild(createEl('h4', '', token.name));
947
-
948
- const meta = createEl('div', 'token-meta');
949
- const prefix = createEl('span', 'token-prefix', token.token_prefix + '...');
950
- meta.appendChild(prefix);
951
- meta.appendChild(document.createTextNode(' Created ' + formatDate(token.created_at)));
952
- info.appendChild(meta);
953
-
954
- const btn = createEl('button', 'btn btn-danger btn-sm', 'Revoke');
955
- btn.addEventListener('click', () => revokeToken(token.id));
956
-
957
- item.appendChild(info);
958
- item.appendChild(btn);
959
- container.appendChild(item);
960
- });
961
- } catch (err) {
962
- container.textContent = '';
963
- const empty = createEl('div', 'empty-state');
964
- empty.appendChild(createEl('p', '', 'Failed to load tokens'));
965
- container.appendChild(empty);
966
- }
967
- }
968
-
969
- function openCreateTokenModal() {
970
- $('createTokenModal').classList.add('active');
971
- $('tokenName').value = '';
972
- $('tokenExpiry').value = '';
973
- }
974
-
975
- function closeModal(id) {
976
- $(id).classList.remove('active');
977
- }
978
-
979
- async function handleCreateToken(e) {
980
- e.preventDefault();
981
- const name = $('tokenName').value;
982
- const expiryVal = $('tokenExpiry').value;
983
- const body = { name };
984
- if (expiryVal) body.expires_in_days = parseInt(expiryVal);
985
-
986
- try {
987
- const res = await fetch(API_BASE + '/tokens', {
988
- method: 'POST',
989
- headers: {
990
- 'Content-Type': 'application/json',
991
- 'Authorization': 'Bearer ' + accessToken
992
- },
993
- body: JSON.stringify(body)
994
- });
995
-
996
- if (!res.ok) throw new Error('Failed');
997
- const data = await res.json();
998
-
999
- closeModal('createTokenModal');
1000
- setText($('newTokenValue'), data.token.token);
1001
- $('tokenCreatedModal').classList.add('active');
1002
- loadTokens();
1003
- } catch (err) {
1004
- alert('Failed to create token');
1005
- }
1006
- }
1007
-
1008
- async function revokeToken(id) {
1009
- if (!confirm('Revoke this token?')) return;
1010
-
1011
- try {
1012
- const res = await fetch(API_BASE + '/tokens/' + id, {
1013
- method: 'DELETE',
1014
- headers: { 'Authorization': 'Bearer ' + accessToken }
1015
- });
1016
-
1017
- if (!res.ok) throw new Error('Failed');
1018
- loadTokens();
1019
- } catch (err) {
1020
- alert('Failed to revoke token');
1021
- }
1022
- }
1023
-
1024
- function copyToken() {
1025
- const token = $('newTokenValue').textContent;
1026
- navigator.clipboard.writeText(token).then(() => {
1027
- alert('Copied!');
1028
- closeModal('tokenCreatedModal');
1029
- });
1030
- }
1031
-
1032
- function toggleSetupGuide() {
1033
- const content = $('setupGuideContent');
1034
- const icon = $('expandIcon');
1035
- content.classList.toggle('expanded');
1036
- icon.classList.toggle('rotated');
1037
- }
1038
-
1039
- function copySettings() {
1040
- const settingsJson = \`{
1041
- "hooks": {
1042
- "SessionStart": [{
1043
- "matcher": "",
1044
- "hooks": [{
1045
- "type": "command",
1046
- "command": "bash .claude/hooks/session-start.sh my-session"
1047
- }]
1048
- }],
1049
- "PreToolUse": [{
1050
- "matcher": "Edit|Write",
1051
- "hooks": [{
1052
- "type": "command",
1053
- "command": "bash .claude/hooks/check-claims.sh"
1054
- }]
1055
- }],
1056
- "PostToolUse": [{
1057
- "matcher": "TodoWrite",
1058
- "hooks": [{
1059
- "type": "command",
1060
- "command": "bash .claude/hooks/todo-sync.sh"
1061
- }]
1062
- }]
1063
- }
1064
- }\`;
1065
- navigator.clipboard.writeText(settingsJson).then(() => {
1066
- const btn = $('copySettingsBtn');
1067
- const originalText = btn.textContent;
1068
- btn.textContent = 'Copied!';
1069
- setTimeout(() => { btn.textContent = originalText; }, 2000);
1070
- });
1071
- }
1072
-
1073
- async function loadSessions() {
1074
- const container = $('sessionList');
1075
- container.textContent = '';
1076
- const loading = createEl('div', 'loading');
1077
- loading.appendChild(createEl('div', 'spinner'));
1078
- container.appendChild(loading);
1079
-
1080
- try {
1081
- const res = await fetch(API_BASE + '/mcp', {
1082
- method: 'POST',
1083
- headers: {
1084
- 'Content-Type': 'application/json',
1085
- 'Authorization': 'Bearer ' + accessToken
1086
- },
1087
- body: JSON.stringify({
1088
- jsonrpc: '2.0',
1089
- id: 1,
1090
- method: 'tools/call',
1091
- params: {
1092
- name: 'collab_session_list',
1093
- arguments: { include_inactive: false }
1094
- }
1095
- })
1096
- });
1097
-
1098
- if (!res.ok) throw new Error('Failed');
1099
- const data = await res.json();
1100
- const result = JSON.parse(data.result.content[0].text);
1101
- container.textContent = '';
1102
-
1103
- if (result.sessions.length === 0) {
1104
- const empty = createEl('div', 'empty-state');
1105
- empty.appendChild(createEl('p', '', 'No active sessions'));
1106
- container.appendChild(empty);
1107
- return;
1108
- }
1109
-
1110
- result.sessions.forEach(session => {
1111
- const item = createEl('div', 'session-item');
1112
-
1113
- // Session header with info and status badge
1114
- const header = createEl('div', 'session-header');
1115
-
1116
- const info = createEl('div', 'session-info');
1117
- info.appendChild(createEl('h4', '', session.name || session.id.slice(0, 8)));
1118
-
1119
- const meta = createEl('div', 'session-meta');
1120
- // Show only the last part of the path for brevity
1121
- const projectName = session.project_root ? session.project_root.split('/').pop() : 'No project';
1122
- meta.textContent = projectName + ' - ' + formatDate(session.last_heartbeat);
1123
- info.appendChild(meta);
1124
-
1125
- const badge = createEl('span', 'status-badge status-' + session.status, session.status);
1126
-
1127
- header.appendChild(info);
1128
- header.appendChild(badge);
1129
- item.appendChild(header);
1130
-
1131
- // Current task display
1132
- if (session.current_task) {
1133
- const taskEl = createEl('div', 'current-task');
1134
- taskEl.appendChild(createEl('span', 'current-task-label', 'Working on:'));
1135
- taskEl.appendChild(createEl('span', '', session.current_task));
1136
- item.appendChild(taskEl);
1137
- }
1138
-
1139
- // Todo list display
1140
- if (session.todos && session.todos.length > 0) {
1141
- const todosEl = createEl('div', 'session-todos');
1142
-
1143
- session.todos.forEach((todo, index) => {
1144
- const todoItem = createEl('div', 'todo-item ' + todo.status);
1145
-
1146
- const statusDot = createEl('span', 'todo-status ' + todo.status);
1147
- const orderNum = createEl('span', 'todo-order', String(index + 1));
1148
- const content = createEl('span', 'todo-content', todo.content);
1149
-
1150
- todoItem.appendChild(statusDot);
1151
- todoItem.appendChild(orderNum);
1152
- todoItem.appendChild(content);
1153
- todosEl.appendChild(todoItem);
1154
- });
1155
-
1156
- item.appendChild(todosEl);
1157
- }
1158
-
1159
- container.appendChild(item);
1160
- });
1161
- } catch (err) {
1162
- container.textContent = '';
1163
- const empty = createEl('div', 'empty-state');
1164
- empty.appendChild(createEl('p', '', 'Failed to load sessions'));
1165
- container.appendChild(empty);
1166
- }
1167
- }
1168
-
1169
- function formatDate(dateStr) {
1170
- const date = new Date(dateStr);
1171
- const diff = Date.now() - date.getTime();
1172
- if (diff < 60000) return 'just now';
1173
- if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
1174
- if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
1175
- return date.toLocaleDateString();
1176
- }
1177
- })();
1178
- </script>
1179
- </body>
1180
- </html>`;
1181
- }