@delt/claude-alarm 0.1.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,580 @@
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>Claude Alarm - Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1117;
10
+ --surface: #1a1d27;
11
+ --border: #2a2d3a;
12
+ --text: #e1e4ed;
13
+ --text-dim: #8b8fa3;
14
+ --accent: #7c6aef;
15
+ --accent-dim: #5a4db8;
16
+ --green: #3dd68c;
17
+ --yellow: #f5c542;
18
+ --red: #ef4444;
19
+ --blue: #60a5fa;
20
+ }
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ min-height: 100vh;
27
+ }
28
+ header {
29
+ border-bottom: 1px solid var(--border);
30
+ padding: 16px 24px;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ }
35
+ header h1 { font-size: 18px; font-weight: 600; }
36
+ .status-badge {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ gap: 6px;
40
+ font-size: 13px;
41
+ color: var(--text-dim);
42
+ }
43
+ .status-dot {
44
+ width: 8px; height: 8px;
45
+ border-radius: 50%;
46
+ background: var(--red);
47
+ }
48
+ .status-dot.connected { background: var(--green); }
49
+
50
+ .container {
51
+ display: grid;
52
+ grid-template-columns: 300px 1fr 320px;
53
+ height: calc(100vh - 57px);
54
+ }
55
+
56
+ /* Sessions panel */
57
+ .sessions-panel {
58
+ border-right: 1px solid var(--border);
59
+ overflow-y: auto;
60
+ padding: 12px;
61
+ }
62
+ .sessions-panel h2 {
63
+ font-size: 13px;
64
+ text-transform: uppercase;
65
+ letter-spacing: 0.5px;
66
+ color: var(--text-dim);
67
+ padding: 8px 8px 12px;
68
+ }
69
+ .session-card {
70
+ background: var(--surface);
71
+ border: 1px solid var(--border);
72
+ border-radius: 8px;
73
+ padding: 12px;
74
+ margin-bottom: 8px;
75
+ cursor: pointer;
76
+ transition: border-color 0.15s;
77
+ }
78
+ .session-card:hover, .session-card.active {
79
+ border-color: var(--accent);
80
+ }
81
+ .session-name {
82
+ font-size: 14px;
83
+ font-weight: 500;
84
+ margin-bottom: 4px;
85
+ }
86
+ .session-id {
87
+ font-size: 11px;
88
+ color: var(--text-dim);
89
+ font-family: monospace;
90
+ }
91
+ .session-status {
92
+ display: inline-block;
93
+ font-size: 11px;
94
+ padding: 2px 8px;
95
+ border-radius: 10px;
96
+ margin-top: 6px;
97
+ font-weight: 500;
98
+ }
99
+ .session-status.idle { background: rgba(139,143,163,0.15); color: var(--text-dim); }
100
+ .session-status.working { background: rgba(96,165,250,0.15); color: var(--blue); }
101
+ .session-status.waiting_input { background: rgba(245,197,66,0.15); color: var(--yellow); }
102
+
103
+ .no-sessions {
104
+ color: var(--text-dim);
105
+ font-size: 13px;
106
+ text-align: center;
107
+ padding: 40px 20px;
108
+ }
109
+
110
+ /* Messages panel */
111
+ .messages-panel {
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+ .messages-header {
116
+ padding: 12px 20px;
117
+ border-bottom: 1px solid var(--border);
118
+ font-size: 14px;
119
+ font-weight: 500;
120
+ }
121
+ .messages-list {
122
+ flex: 1;
123
+ overflow-y: auto;
124
+ padding: 16px 20px;
125
+ }
126
+ .message {
127
+ margin-bottom: 12px;
128
+ padding: 10px 14px;
129
+ border-radius: 8px;
130
+ font-size: 13px;
131
+ line-height: 1.5;
132
+ max-width: 80%;
133
+ }
134
+ .message.from-session {
135
+ background: var(--surface);
136
+ border: 1px solid var(--border);
137
+ }
138
+ .message.from-dashboard {
139
+ background: rgba(124,106,239,0.12);
140
+ border: 1px solid rgba(124,106,239,0.25);
141
+ margin-left: auto;
142
+ }
143
+ .message-meta {
144
+ font-size: 11px;
145
+ color: var(--text-dim);
146
+ margin-bottom: 4px;
147
+ }
148
+ .message-input-area {
149
+ border-top: 1px solid var(--border);
150
+ padding: 12px 20px;
151
+ display: flex;
152
+ gap: 8px;
153
+ }
154
+ .message-input-area input {
155
+ flex: 1;
156
+ background: var(--surface);
157
+ border: 1px solid var(--border);
158
+ border-radius: 6px;
159
+ padding: 8px 12px;
160
+ color: var(--text);
161
+ font-size: 13px;
162
+ outline: none;
163
+ }
164
+ .message-input-area input:focus { border-color: var(--accent); }
165
+ .message-input-area button {
166
+ background: var(--accent);
167
+ color: white;
168
+ border: none;
169
+ border-radius: 6px;
170
+ padding: 8px 16px;
171
+ cursor: pointer;
172
+ font-size: 13px;
173
+ font-weight: 500;
174
+ }
175
+ .message-input-area button:hover { background: var(--accent-dim); }
176
+ .message-input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
177
+
178
+ /* Notifications panel */
179
+ .notifications-panel {
180
+ border-left: 1px solid var(--border);
181
+ overflow-y: auto;
182
+ padding: 12px;
183
+ }
184
+ .notifications-panel h2 {
185
+ font-size: 13px;
186
+ text-transform: uppercase;
187
+ letter-spacing: 0.5px;
188
+ color: var(--text-dim);
189
+ padding: 8px 8px 12px;
190
+ }
191
+ .notif-item {
192
+ background: var(--surface);
193
+ border: 1px solid var(--border);
194
+ border-radius: 8px;
195
+ padding: 10px 12px;
196
+ margin-bottom: 8px;
197
+ font-size: 13px;
198
+ }
199
+ .notif-item .notif-title {
200
+ font-weight: 500;
201
+ margin-bottom: 2px;
202
+ }
203
+ .notif-item .notif-message { color: var(--text-dim); }
204
+ .notif-item .notif-time {
205
+ font-size: 11px;
206
+ color: var(--text-dim);
207
+ margin-top: 4px;
208
+ }
209
+ .notif-level {
210
+ display: inline-block;
211
+ width: 6px; height: 6px;
212
+ border-radius: 50%;
213
+ margin-right: 6px;
214
+ }
215
+ .notif-level.info { background: var(--blue); }
216
+ .notif-level.success { background: var(--green); }
217
+ .notif-level.warning { background: var(--yellow); }
218
+ .notif-level.error { background: var(--red); }
219
+
220
+ .empty-state {
221
+ color: var(--text-dim);
222
+ font-size: 13px;
223
+ text-align: center;
224
+ padding: 40px 20px;
225
+ }
226
+
227
+ /* Token auth overlay */
228
+ .token-overlay {
229
+ position: fixed;
230
+ inset: 0;
231
+ background: rgba(0,0,0,0.8);
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ z-index: 100;
236
+ }
237
+ .token-overlay.hidden { display: none; }
238
+ .token-form {
239
+ background: var(--surface);
240
+ border: 1px solid var(--border);
241
+ border-radius: 12px;
242
+ padding: 32px;
243
+ max-width: 400px;
244
+ width: 90%;
245
+ text-align: center;
246
+ }
247
+ .token-form h2 { font-size: 18px; margin-bottom: 8px; }
248
+ .token-form p { font-size: 13px; color: var(--text-dim); margin-bottom: 20px; }
249
+ .token-form input {
250
+ width: 100%;
251
+ background: var(--bg);
252
+ border: 1px solid var(--border);
253
+ border-radius: 6px;
254
+ padding: 10px 12px;
255
+ color: var(--text);
256
+ font-size: 14px;
257
+ font-family: monospace;
258
+ outline: none;
259
+ margin-bottom: 12px;
260
+ }
261
+ .token-form input:focus { border-color: var(--accent); }
262
+ .token-form button {
263
+ background: var(--accent);
264
+ color: white;
265
+ border: none;
266
+ border-radius: 6px;
267
+ padding: 10px 24px;
268
+ cursor: pointer;
269
+ font-size: 14px;
270
+ font-weight: 500;
271
+ }
272
+ .token-form button:hover { background: var(--accent-dim); }
273
+ .token-error { color: var(--red); font-size: 12px; margin-top: 8px; display: none; }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <header>
278
+ <h1>Claude Alarm</h1>
279
+ <div class="status-badge">
280
+ <span class="status-dot" id="connDot"></span>
281
+ <span id="connLabel">Connecting...</span>
282
+ </div>
283
+ </header>
284
+
285
+ <div class="token-overlay hidden" id="tokenOverlay">
286
+ <div class="token-form">
287
+ <h2>Authentication Required</h2>
288
+ <p>Enter the hub token to connect. Find it by running: <code>claude-alarm token</code></p>
289
+ <input type="text" id="tokenInput" placeholder="Paste token here..." autocomplete="off">
290
+ <button id="tokenSubmit">Connect</button>
291
+ <div class="token-error" id="tokenError">Connection failed. Check your token.</div>
292
+ </div>
293
+ </div>
294
+
295
+ <div class="container">
296
+ <!-- Sessions -->
297
+ <div class="sessions-panel">
298
+ <h2>Sessions</h2>
299
+ <div id="sessionsList"></div>
300
+ </div>
301
+
302
+ <!-- Messages -->
303
+ <div class="messages-panel">
304
+ <div class="messages-header" id="messagesHeader">Select a session</div>
305
+ <div class="messages-list" id="messagesList">
306
+ <div class="empty-state">Select a session to view messages</div>
307
+ </div>
308
+ <div class="message-input-area">
309
+ <input type="text" id="msgInput" placeholder="Send a message to session..." disabled>
310
+ <button id="sendBtn" disabled>Send</button>
311
+ </div>
312
+ </div>
313
+
314
+ <!-- Notifications -->
315
+ <div class="notifications-panel">
316
+ <h2>Notifications</h2>
317
+ <div id="notifList">
318
+ <div class="empty-state">No notifications yet</div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <script>
324
+ (function() {
325
+ const state = {
326
+ ws: null,
327
+ sessions: {},
328
+ selectedSession: null,
329
+ messages: {}, // sessionId -> [{from, content, time}]
330
+ notifications: [],
331
+ token: null,
332
+ };
333
+
334
+ const $ = (sel) => document.querySelector(sel);
335
+
336
+ // --- Token handling ---
337
+ function getToken() {
338
+ // Check URL query parameter first
339
+ const params = new URLSearchParams(location.search);
340
+ const urlToken = params.get('token');
341
+ if (urlToken) {
342
+ sessionStorage.setItem('claude-alarm-token', urlToken);
343
+ return urlToken;
344
+ }
345
+ // Check sessionStorage
346
+ return sessionStorage.getItem('claude-alarm-token');
347
+ }
348
+
349
+ function showTokenForm() {
350
+ $('#tokenOverlay').classList.remove('hidden');
351
+ }
352
+
353
+ function hideTokenForm() {
354
+ $('#tokenOverlay').classList.add('hidden');
355
+ }
356
+
357
+ $('#tokenSubmit').addEventListener('click', () => {
358
+ const token = $('#tokenInput').value.trim();
359
+ if (!token) return;
360
+ state.token = token;
361
+ sessionStorage.setItem('claude-alarm-token', token);
362
+ $('#tokenError').style.display = 'none';
363
+ hideTokenForm();
364
+ connect();
365
+ });
366
+
367
+ $('#tokenInput').addEventListener('keydown', (e) => {
368
+ if (e.key === 'Enter') $('#tokenSubmit').click();
369
+ });
370
+
371
+ // --- WebSocket ---
372
+ function connect() {
373
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
374
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
375
+ const ws = new WebSocket(`${proto}://${location.host}/ws/dashboard${tokenQuery}`);
376
+
377
+ ws.onopen = () => {
378
+ state.ws = ws;
379
+ hideTokenForm();
380
+ $('#connDot').classList.add('connected');
381
+ $('#connLabel').textContent = 'Connected';
382
+ };
383
+
384
+ ws.onclose = () => {
385
+ state.ws = null;
386
+ $('#connDot').classList.remove('connected');
387
+ $('#connLabel').textContent = 'Disconnected';
388
+ setTimeout(connect, 3000);
389
+ };
390
+
391
+ ws.onerror = () => {
392
+ // If connection fails, it might be a token issue
393
+ if (!state.token) {
394
+ showTokenForm();
395
+ } else {
396
+ $('#tokenError').style.display = 'block';
397
+ showTokenForm();
398
+ }
399
+ ws.close();
400
+ };
401
+
402
+ ws.onmessage = (e) => {
403
+ try { handleMessage(JSON.parse(e.data)); } catch {}
404
+ };
405
+ }
406
+
407
+ function handleMessage(msg) {
408
+ switch (msg.type) {
409
+ case 'sessions_list':
410
+ state.sessions = {};
411
+ msg.sessions.forEach(s => { state.sessions[s.id] = s; });
412
+ renderSessions();
413
+ break;
414
+
415
+ case 'session_connected':
416
+ state.sessions[msg.session.id] = msg.session;
417
+ if (!state.messages[msg.session.id]) state.messages[msg.session.id] = [];
418
+ renderSessions();
419
+ break;
420
+
421
+ case 'session_disconnected':
422
+ delete state.sessions[msg.sessionId];
423
+ if (state.selectedSession === msg.sessionId) {
424
+ state.selectedSession = null;
425
+ renderMessages();
426
+ }
427
+ renderSessions();
428
+ break;
429
+
430
+ case 'session_updated':
431
+ state.sessions[msg.session.id] = msg.session;
432
+ renderSessions();
433
+ break;
434
+
435
+ case 'reply_from_session':
436
+ if (!state.messages[msg.sessionId]) state.messages[msg.sessionId] = [];
437
+ state.messages[msg.sessionId].push({
438
+ from: 'session',
439
+ content: msg.content,
440
+ time: msg.timestamp,
441
+ });
442
+ if (state.selectedSession === msg.sessionId) renderMessages();
443
+ break;
444
+
445
+ case 'notification':
446
+ state.notifications.unshift({
447
+ sessionId: msg.sessionId,
448
+ title: msg.title,
449
+ message: msg.message,
450
+ level: msg.level || 'info',
451
+ time: msg.timestamp,
452
+ });
453
+ renderNotifications();
454
+ break;
455
+ }
456
+ }
457
+
458
+ // --- Render ---
459
+ function renderSessions() {
460
+ const el = $('#sessionsList');
461
+ const ids = Object.keys(state.sessions);
462
+ if (!ids.length) {
463
+ el.innerHTML = '<div class="no-sessions">No active sessions.<br>Start Claude Code with the channel to see sessions here.</div>';
464
+ return;
465
+ }
466
+ el.innerHTML = ids.map(id => {
467
+ const s = state.sessions[id];
468
+ const active = state.selectedSession === id ? ' active' : '';
469
+ return `<div class="session-card${active}" data-id="${id}">
470
+ <div class="session-name">${esc(s.name)}</div>
471
+ <span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
472
+ </div>`;
473
+ }).join('');
474
+
475
+ el.querySelectorAll('.session-card').forEach(card => {
476
+ card.addEventListener('click', () => selectSession(card.dataset.id));
477
+ });
478
+ }
479
+
480
+ function selectSession(id) {
481
+ state.selectedSession = id;
482
+ if (!state.messages[id]) state.messages[id] = [];
483
+ $('#msgInput').disabled = false;
484
+ $('#sendBtn').disabled = false;
485
+ renderSessions();
486
+ renderMessages();
487
+ }
488
+
489
+ function renderMessages() {
490
+ const el = $('#messagesList');
491
+ const header = $('#messagesHeader');
492
+
493
+ if (!state.selectedSession) {
494
+ header.textContent = 'Select a session';
495
+ el.innerHTML = '<div class="empty-state">Select a session to view messages</div>';
496
+ $('#msgInput').disabled = true;
497
+ $('#sendBtn').disabled = true;
498
+ return;
499
+ }
500
+
501
+ const s = state.sessions[state.selectedSession];
502
+ header.textContent = s ? s.name : state.selectedSession.slice(0, 12);
503
+
504
+ const msgs = state.messages[state.selectedSession] || [];
505
+ if (!msgs.length) {
506
+ el.innerHTML = '<div class="empty-state">No messages yet</div>';
507
+ return;
508
+ }
509
+
510
+ el.innerHTML = msgs.map(m => {
511
+ const cls = m.from === 'session' ? 'from-session' : 'from-dashboard';
512
+ const timeStr = new Date(m.time).toLocaleTimeString();
513
+ return `<div class="message ${cls}">
514
+ <div class="message-meta">${m.from === 'session' ? 'Claude' : 'You'} &middot; ${timeStr}</div>
515
+ ${esc(m.content)}
516
+ </div>`;
517
+ }).join('');
518
+
519
+ el.scrollTop = el.scrollHeight;
520
+ }
521
+
522
+ function renderNotifications() {
523
+ const el = $('#notifList');
524
+ if (!state.notifications.length) {
525
+ el.innerHTML = '<div class="empty-state">No notifications yet</div>';
526
+ return;
527
+ }
528
+ el.innerHTML = state.notifications.slice(0, 50).map(n => {
529
+ const timeStr = new Date(n.time).toLocaleTimeString();
530
+ const session = state.sessions[n.sessionId];
531
+ const sName = session ? session.name : n.sessionId.slice(0, 8);
532
+ return `<div class="notif-item">
533
+ <div class="notif-title"><span class="notif-level ${n.level}"></span>${esc(n.title)}</div>
534
+ <div class="notif-message">${esc(n.message)}</div>
535
+ <div class="notif-time">${sName} &middot; ${timeStr}</div>
536
+ </div>`;
537
+ }).join('');
538
+ }
539
+
540
+ // --- Send ---
541
+ function sendMessage() {
542
+ const input = $('#msgInput');
543
+ const content = input.value.trim();
544
+ if (!content || !state.selectedSession || !state.ws) return;
545
+
546
+ state.ws.send(JSON.stringify({
547
+ type: 'message_to_session',
548
+ sessionId: state.selectedSession,
549
+ content,
550
+ }));
551
+
552
+ if (!state.messages[state.selectedSession]) state.messages[state.selectedSession] = [];
553
+ state.messages[state.selectedSession].push({
554
+ from: 'dashboard',
555
+ content,
556
+ time: Date.now(),
557
+ });
558
+
559
+ input.value = '';
560
+ renderMessages();
561
+ }
562
+
563
+ $('#sendBtn').addEventListener('click', sendMessage);
564
+ $('#msgInput').addEventListener('keydown', (e) => {
565
+ if (e.key === 'Enter') sendMessage();
566
+ });
567
+
568
+ function esc(s) {
569
+ const d = document.createElement('div');
570
+ d.textContent = s;
571
+ return d.innerHTML;
572
+ }
573
+
574
+ // Start - try to get token from URL or storage
575
+ state.token = getToken();
576
+ connect();
577
+ })();
578
+ </script>
579
+ </body>
580
+ </html>