@delt/claude-alarm 0.3.12 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -119,6 +119,22 @@
119
119
  font-size: 14px;
120
120
  font-weight: 500;
121
121
  margin-bottom: 4px;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 6px;
125
+ }
126
+ .unread-badge {
127
+ background: var(--accent);
128
+ color: #fff;
129
+ font-size: 10px;
130
+ font-weight: 600;
131
+ min-width: 18px;
132
+ height: 18px;
133
+ border-radius: 9px;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ padding: 0 5px;
122
138
  }
123
139
  .session-status {
124
140
  display: inline-block;
@@ -474,12 +490,61 @@
474
490
  }
475
491
  .token-form button:hover { background: var(--accent-dim); }
476
492
  .token-error { color: var(--red); font-size: 12px; margin-top: 8px; display: none; }
493
+
494
+ /* Mobile tabs */
495
+ .mobile-tabs {
496
+ display: none;
497
+ border-bottom: 1px solid var(--border);
498
+ }
499
+ .mobile-tabs button {
500
+ flex: 1;
501
+ background: none;
502
+ border: none;
503
+ border-bottom: 2px solid transparent;
504
+ color: var(--text-dim);
505
+ padding: 10px;
506
+ font-size: 13px;
507
+ cursor: pointer;
508
+ }
509
+ .mobile-tabs button.active {
510
+ color: var(--accent);
511
+ border-bottom-color: var(--accent);
512
+ }
513
+
514
+ /* Mobile responsive */
515
+ @media (max-width: 768px) {
516
+ .container {
517
+ grid-template-columns: 1fr;
518
+ height: calc(100vh - 97px);
519
+ }
520
+ .sessions-panel, .messages-panel, .notifications-panel {
521
+ border: none;
522
+ display: none;
523
+ }
524
+ .sessions-panel.mobile-active,
525
+ .messages-panel.mobile-active,
526
+ .notifications-panel.mobile-active {
527
+ display: flex;
528
+ flex-direction: column;
529
+ }
530
+ .sessions-panel.mobile-active {
531
+ display: block;
532
+ overflow-y: auto;
533
+ }
534
+ .notifications-panel.mobile-active {
535
+ display: block;
536
+ overflow-y: auto;
537
+ }
538
+ .mobile-tabs { display: flex; }
539
+ .message { max-width: 95%; }
540
+ }
477
541
  </style>
478
542
  </head>
479
543
  <body>
480
544
  <header>
481
545
  <h1>Claude Alarm</h1>
482
546
  <div class="header-right">
547
+ <button class="theme-toggle" id="webhookToggle" title="Webhook settings">&#9881;</button>
483
548
  <button class="theme-toggle" id="themeToggle" title="Toggle theme">&#9790;</button>
484
549
  <div class="status-badge">
485
550
  <span class="status-dot" id="connDot"></span>
@@ -498,6 +563,24 @@
498
563
  </div>
499
564
  </div>
500
565
 
566
+ <div class="token-overlay hidden" id="webhookOverlay">
567
+ <div class="token-form" style="max-width:500px;text-align:left">
568
+ <h2 style="text-align:center">Webhook Settings</h2>
569
+ <p style="text-align:center">Add webhook URLs to receive notifications externally.</p>
570
+ <div id="webhookList"></div>
571
+ <button id="webhookAdd" style="width:100%;margin-top:8px;background:var(--surface);color:var(--text);border:1px dashed var(--border);border-radius:6px;padding:8px;cursor:pointer;font-size:13px">+ Add Webhook</button>
572
+ <div style="display:flex;gap:8px;margin-top:16px">
573
+ <button id="webhookSave" style="flex:1">Save</button>
574
+ <button id="webhookCancel" style="flex:1;background:var(--surface);color:var(--text);border:1px solid var(--border)">Cancel</button>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="mobile-tabs" id="mobileTabs">
580
+ <button class="active" data-tab="sessions">Sessions</button>
581
+ <button data-tab="messages">Messages</button>
582
+ <button data-tab="notifications">Notifications</button>
583
+ </div>
501
584
  <div class="container">
502
585
  <div class="sessions-panel">
503
586
  <h2>Sessions</h2>
@@ -545,6 +628,7 @@
545
628
  notifications: [],
546
629
  token: null,
547
630
  pendingImage: null,
631
+ unread: {},
548
632
  };
549
633
 
550
634
  const $ = (sel) => document.querySelector(sel);
@@ -644,7 +728,8 @@
644
728
  case 'reply_from_session':
645
729
  if (!state.messages[msg.sessionId]) state.messages[msg.sessionId] = [];
646
730
  state.messages[msg.sessionId].push({ from: 'session', content: msg.content, time: msg.timestamp });
647
- if (state.selectedSession === msg.sessionId) renderMessages();
731
+ if (state.selectedSession === msg.sessionId) { renderMessages(); }
732
+ else { state.unread[msg.sessionId] = (state.unread[msg.sessionId] || 0) + 1; renderSessions(); }
648
733
  state.notifications.unshift({ sessionId: msg.sessionId, title: 'Reply', message: msg.content.slice(0, 100), level: 'info', time: msg.timestamp });
649
734
  renderNotifications();
650
735
  break;
@@ -690,8 +775,9 @@
690
775
  const s = state.sessions[id];
691
776
  const active = state.selectedSession === id ? ' active' : '';
692
777
  const cwdDisplay = s.cwd ? s.cwd.replace(/^.*[/\\]/, '') : '';
778
+ const unread = state.unread[id] || 0;
693
779
  return `<div class="session-card${active}" data-id="${id}">
694
- <div class="session-name">${esc(s.name)}</div>
780
+ <div class="session-name">${esc(s.name)}${unread ? `<span class="unread-badge">${unread}</span>` : ''}</div>
695
781
  ${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)}</div>` : ''}
696
782
  <span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
697
783
  </div>`;
@@ -704,6 +790,7 @@
704
790
 
705
791
  function selectSession(id) {
706
792
  state.selectedSession = id;
793
+ state.unread[id] = 0;
707
794
  if (!state.messages[id]) state.messages[id] = [];
708
795
  $('#msgInput').disabled = false;
709
796
  $('#sendBtn').disabled = false;
@@ -742,7 +829,7 @@
742
829
 
743
830
  el.innerHTML = msgs.map(m => {
744
831
  const cls = m.from === 'session' ? 'from-session' : 'from-dashboard';
745
- const timeStr = new Date(m.time).toLocaleTimeString();
832
+ const timeStr = relativeTime(m.time);
746
833
  let content;
747
834
  if (m.imageData) {
748
835
  content = `<img src="${m.imageData}" alt="${esc(m.imageName || 'image')}">`;
@@ -769,7 +856,7 @@
769
856
  }
770
857
  clearBtn.style.display = 'block';
771
858
  el.innerHTML = state.notifications.slice(0, 50).map((n, i) => {
772
- const timeStr = new Date(n.time).toLocaleTimeString();
859
+ const timeStr = relativeTime(n.time);
773
860
  const session = state.sessions[n.sessionId];
774
861
  const sName = session ? session.name : n.sessionId.slice(0, 8);
775
862
  return `<div class="notif-item" data-session="${n.sessionId}" data-index="${i}">
@@ -911,6 +998,15 @@
911
998
  this.style.height = Math.min(this.scrollHeight, 120) + 'px';
912
999
  });
913
1000
 
1001
+ function relativeTime(ts) {
1002
+ const diff = Math.floor((Date.now() - ts) / 1000);
1003
+ if (diff < 5) return 'just now';
1004
+ if (diff < 60) return diff + 's ago';
1005
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
1006
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
1007
+ return new Date(ts).toLocaleDateString();
1008
+ }
1009
+
914
1010
  function esc(s) {
915
1011
  const d = document.createElement('div');
916
1012
  d.textContent = s;
@@ -959,6 +1055,64 @@
959
1055
  msgList.scrollTo({ top: msgList.scrollHeight, behavior: 'smooth' });
960
1056
  });
961
1057
 
1058
+ // --- Webhook settings ---
1059
+ let webhookData = [];
1060
+ $('#webhookToggle').addEventListener('click', async () => {
1061
+ try {
1062
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1063
+ const res = await fetch(`/api/webhooks${tokenQuery}`);
1064
+ const data = await res.json();
1065
+ webhookData = data.webhooks || [];
1066
+ } catch { webhookData = []; }
1067
+ renderWebhooks();
1068
+ $('#webhookOverlay').classList.remove('hidden');
1069
+ });
1070
+ $('#webhookCancel').addEventListener('click', () => $('#webhookOverlay').classList.add('hidden'));
1071
+ $('#webhookAdd').addEventListener('click', () => {
1072
+ webhookData.push({ url: '' });
1073
+ renderWebhooks();
1074
+ });
1075
+ $('#webhookSave').addEventListener('click', async () => {
1076
+ const valid = webhookData.filter(w => w.url.trim());
1077
+ try {
1078
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1079
+ await fetch(`/api/webhooks${tokenQuery}`, {
1080
+ method: 'POST',
1081
+ headers: { 'Content-Type': 'application/json' },
1082
+ body: JSON.stringify({ webhooks: valid }),
1083
+ });
1084
+ } catch {}
1085
+ $('#webhookOverlay').classList.add('hidden');
1086
+ });
1087
+ function renderWebhooks() {
1088
+ const el = $('#webhookList');
1089
+ el.innerHTML = webhookData.map((w, i) => `<div style="display:flex;gap:6px;margin-bottom:6px">
1090
+ <input type="text" value="${esc(w.url)}" placeholder="https://hooks.slack.com/..." style="flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px;color:var(--text);font-size:13px;outline:none" data-idx="${i}">
1091
+ <button onclick="this.parentElement.remove()" style="background:none;border:1px solid var(--border);border-radius:6px;color:var(--red);cursor:pointer;padding:4px 8px">&times;</button>
1092
+ </div>`).join('');
1093
+ el.querySelectorAll('input').forEach(inp => {
1094
+ inp.addEventListener('input', () => { webhookData[parseInt(inp.dataset.idx)].url = inp.value; });
1095
+ });
1096
+ }
1097
+
1098
+ // Refresh relative times every 30s
1099
+ setInterval(() => { renderMessages(); renderNotifications(); }, 30000);
1100
+
1101
+ // Mobile tabs
1102
+ document.querySelectorAll('#mobileTabs button').forEach(btn => {
1103
+ btn.addEventListener('click', () => {
1104
+ document.querySelectorAll('#mobileTabs button').forEach(b => b.classList.remove('active'));
1105
+ btn.classList.add('active');
1106
+ const tab = btn.dataset.tab;
1107
+ document.querySelectorAll('.sessions-panel, .messages-panel, .notifications-panel').forEach(p => p.classList.remove('mobile-active'));
1108
+ document.querySelector(`.${tab === 'sessions' ? 'sessions' : tab === 'messages' ? 'messages' : 'notifications'}-panel`).classList.add('mobile-active');
1109
+ });
1110
+ });
1111
+ // Default mobile tab
1112
+ if (window.innerWidth <= 768) {
1113
+ document.querySelector('.sessions-panel').classList.add('mobile-active');
1114
+ }
1115
+
962
1116
  state.token = getToken();
963
1117
  connect();
964
1118
  })();