@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.
- package/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +158 -4
- package/dist/hub/server.js +87 -20
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +111 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +158 -4
package/src/dashboard/index.html
CHANGED
|
@@ -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">⚙</button>
|
|
483
548
|
<button class="theme-toggle" id="themeToggle" title="Toggle theme">☾</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 =
|
|
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 =
|
|
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">×</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
|
})();
|