omni-notify-mcp 1.3.14 → 1.3.21

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,538 +1,538 @@
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 Notify</title>
7
- <link rel="icon" href="assets/logo.svg" type="image/svg+xml">
8
- <link rel="stylesheet" href="style.css">
9
- </head>
10
- <body>
11
- <div id="toast" class="toast hidden"></div>
12
-
13
- <header>
14
- <div class="logo">
15
- <img src="assets/logo.svg" width="22" height="22" alt="">
16
- omni-notify-mcp
17
- </div>
18
- <span class="subtitle">Multi-channel notifications for AI agents</span>
19
- <a href="/help.html" class="header-link" style="margin-left:auto">Help &amp; setup →</a>
20
- </header>
21
-
22
- <main>
23
- <div class="mute-all-bar" id="mute-all-bar">
24
- <div class="mute-all-main">
25
- <span class="mute-all-icon">🔕</span>
26
- <div class="mute-all-text">
27
- <span class="mute-all-title">Disable all notifications</span>
28
- <span class="mute-all-sub" id="mute-all-sub">Master kill switch — silences every channel &amp; client, including high-priority. Overrides DND.</span>
29
- </div>
30
- </div>
31
- <label class="toggle-wrap">
32
- <input type="checkbox" id="mute-all" onchange="saveMuteAll()">
33
- <span class="toggle"></span>
34
- </label>
35
- </div>
36
- <div class="config-panel">
37
- <section class="section section-channels">
38
- <div class="section-hd">
39
- <span class="section-hd-title">Delivery channels</span>
40
- <span class="section-hd-sub">Where notifications go</span>
41
- </div>
42
- <div class="card-list" id="card-list-channels">
43
-
44
- <!-- ── Desktop ───────────────────────────────────────────────── -->
45
- <div class="card" id="card-desktop">
46
- <div class="card-hd" onclick="toggleCard('desktop')">
47
- <div class="ch-meta">
48
- <div class="channel-icon desktop-icon">
49
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
50
- </div>
51
- <span class="ch-name">Desktop</span>
52
- </div>
53
- <span class="badge badge-idle" id="badge-desktop">–</span>
54
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
55
- </div>
56
- <div class="card-body">
57
- <div class="actions">
58
- <label class="toggle-wrap">
59
- <input type="checkbox" id="desktop-enabled" onchange="saveDesktop()">
60
- <span class="toggle"></span>
61
- </label>
62
- <span class="toggle-lbl">Enable</span>
63
- <button class="btn btn-sm btn-ghost" onclick="testChannel('desktop')">Test</button>
64
- </div>
65
- <div class="actions" style="margin-top:6px">
66
- <label class="toggle-wrap">
67
- <input type="checkbox" id="desktop-sound" onchange="saveDesktop()">
68
- <span class="toggle"></span>
69
- </label>
70
- <span class="toggle-lbl">Play system sound</span>
71
- <button class="btn btn-sm btn-ghost" onclick="testSound()">Test sound</button>
72
- </div>
73
- <div class="actions" style="margin-top:6px">
74
- <label class="toggle-wrap">
75
- <input type="checkbox" id="desktop-tts" onchange="saveDesktop()">
76
- <span class="toggle"></span>
77
- </label>
78
- <span class="toggle-lbl">Speak notification</span>
79
- <button class="btn btn-sm btn-ghost" onclick="testTts()">Test voice</button>
80
- </div>
81
- <div id="tts-voice-row" class="fg" style="margin-top:8px; display:none">
82
- <label>Voice</label>
83
- <select id="desktop-tts-voice" onchange="saveDesktop()"></select>
84
- </div>
85
- <span id="os-hint" class="os-tag hidden"></span>
86
- </div>
87
- </div>
88
-
89
- <!-- ── Email ─────────────────────────────────────────────────── -->
90
- <div class="card" id="card-email">
91
- <div class="card-hd" onclick="toggleCard('email')">
92
- <div class="ch-meta">
93
- <div class="channel-icon gmail-icon">
94
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
95
- </div>
96
- <span class="ch-name">Email</span><span class="tag">Gmail</span>
97
- </div>
98
- <span class="badge badge-idle" id="badge-email">Not configured</span>
99
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
100
- </div>
101
- <div class="card-body">
102
- <!-- Connected -->
103
- <div id="gmail-connected-state" class="hidden">
104
- <div class="actions">
105
- <label class="toggle-wrap">
106
- <input type="checkbox" id="email-enabled" onchange="saveEmail()">
107
- <span class="toggle"></span>
108
- </label>
109
- <span class="toggle-lbl">Enable</span>
110
- <button class="btn btn-sm btn-ghost" onclick="testChannel('email')">Test</button>
111
- </div>
112
- <div class="ok-banner">
113
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
114
- <strong id="gmail-connected-email"></strong>
115
- </div>
116
- <div class="fg"><label>Send to</label><input type="email" id="gmail-to-connected" placeholder="you@gmail.com" oninput="saveEmailDebounced()"></div>
117
- </div>
118
- <!-- Setup -->
119
- <div id="gmail-setup-state">
120
- <div class="actions">
121
- <button class="btn btn-sm btn-ghost" onclick="openAppPasswords()">Open Google Account</button>
122
- <button class="btn btn-sm btn-primary" onclick="saveAppPassword()">Connect</button>
123
- </div>
124
- <details class="guide" id="gmail-guide">
125
- <summary>How to get an App Password</summary>
126
- <ol>
127
- <li>Click <b>Open Google Account</b> above (needs 2FA enabled)</li>
128
- <li>Select app: <b>Mail</b>, device: <b>Mac</b> → Generate</li>
129
- <li>Copy the 16-char password and paste below</li>
130
- </ol>
131
- </details>
132
- <div class="fg"><label>Gmail address</label><input type="email" id="gmail-address" placeholder="you@gmail.com"></div>
133
- <div class="fg"><label>App Password (16 chars)</label><input type="password" id="gmail-app-password" placeholder="xxxx xxxx xxxx xxxx"></div>
134
- </div>
135
- </div>
136
- </div>
137
-
138
- <!-- ── Telegram ──────────────────────────────────────────────── -->
139
- <div class="card" id="card-telegram">
140
- <div class="card-hd" onclick="toggleCard('telegram')">
141
- <div class="ch-meta">
142
- <div class="channel-icon telegram-icon">
143
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
144
- </div>
145
- <span class="ch-name">Telegram</span><span class="tag">Bot API</span>
146
- </div>
147
- <span class="badge badge-idle" id="badge-telegram">Not configured</span>
148
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
149
- </div>
150
- <div class="card-body">
151
- <div class="actions">
152
- <label class="toggle-wrap">
153
- <input type="checkbox" id="telegram-enabled" onchange="saveTelegram()">
154
- <span class="toggle"></span>
155
- </label>
156
- <span class="toggle-lbl">Enable</span>
157
- <button class="btn btn-sm btn-ghost" onclick="testChannel('telegram')">Test</button>
158
- </div>
159
- <details class="guide">
160
- <summary>Create a bot (2 min)</summary>
161
- <ol>
162
- <li>Open Telegram → search <b>@BotFather</b></li>
163
- <li>Send <code class="cp" onclick="copyText(this)">/newbot</code> → follow prompts → copy token</li>
164
- <li>From each chat/group you want notified, send your bot any message (add it to groups first)</li>
165
- <li>Paste token below → click <b>Detect chats</b> → tick the chats</li>
166
- </ol>
167
- </details>
168
- <div class="fg"><label>Bot Token</label><input type="password" id="telegram-token" placeholder="123456:ABC-DEF…" oninput="saveTelegramDebounced()"></div>
169
- <div class="fg">
170
- <label>Chats to notify</label>
171
- <div id="telegram-chips" class="chips"></div>
172
- <div class="dest-add">
173
- <button class="btn btn-sm btn-primary" onclick="detectTelegramChats()">Detect chats</button>
174
- <input type="text" id="telegram-chat-manual" placeholder="or paste chat ID" style="flex:1">
175
- <button class="btn btn-sm btn-ghost" onclick="addTelegramChatManual()">Add</button>
176
- </div>
177
- <div id="telegram-chat-picker" class="picker hidden"></div>
178
- </div>
179
- </div>
180
- </div>
181
-
182
- <!-- ── SMS ───────────────────────────────────────────────────── -->
183
- <div class="card" id="card-sms">
184
- <div class="card-hd" onclick="toggleCard('sms')">
185
- <div class="ch-meta">
186
- <div class="channel-icon sms-icon">
187
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
188
- </div>
189
- <span class="ch-name">SMS</span><span class="tag">AWS</span>
190
- </div>
191
- <span class="badge badge-idle" id="badge-sms">Not configured</span>
192
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
193
- </div>
194
- <div class="card-body">
195
- <div class="actions">
196
- <label class="toggle-wrap">
197
- <input type="checkbox" id="sms-enabled" onchange="saveSms()">
198
- <span class="toggle"></span>
199
- </label>
200
- <span class="toggle-lbl">Enable</span>
201
- <button class="btn btn-sm btn-ghost" onclick="testChannel('sms')">Test</button>
202
- </div>
203
- <div id="sms-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
204
- <details class="guide">
205
- <summary>AWS End User Messaging credentials</summary>
206
- <ol>
207
- <li>AWS Console → IAM user with <code class="cp" onclick="copyText(this)">sms-voice:SendTextMessage</code> → copy <b>Access Key ID</b> + <b>Secret</b></li>
208
- <li>AWS End User Messaging → Phone numbers → request a number → it becomes your origination number</li>
209
- <li>Paste below → <b>Discover</b> to auto-fill your numbers → add recipients</li>
210
- </ol>
211
- </details>
212
- <div class="row2">
213
- <div class="fg"><label>Access Key ID</label><input type="text" id="sms-accesskey" placeholder="AKIA…" oninput="saveSmsDebounced()"></div>
214
- <div class="fg"><label>Secret Access Key</label><input type="password" id="sms-secret" placeholder="••••••••" oninput="saveSmsDebounced()"></div>
215
- <div class="fg"><label>Region</label><input type="text" id="sms-region" placeholder="us-east-1" oninput="saveSmsDebounced()"></div>
216
- <div class="fg">
217
- <label>From (origination number)</label>
218
- <div class="dest-add">
219
- <input type="text" id="sms-from" list="sms-from-list" placeholder="+1… or auto" oninput="saveSmsDebounced()" style="flex:1">
220
- <datalist id="sms-from-list"></datalist>
221
- <button class="btn btn-sm btn-ghost" onclick="discoverSmsNumbers()">Discover</button>
222
- </div>
223
- </div>
224
- </div>
225
- <div class="fg">
226
- <label>Send to (recipients)</label>
227
- <div id="sms-chips" class="chips"></div>
228
- <div class="dest-add">
229
- <input type="tel" id="sms-to-manual" placeholder="+1 234 567 8900" style="flex:1">
230
- <button class="btn btn-sm btn-ghost" onclick="addSmsNumberManual()">Add</button>
231
- </div>
232
- <div id="sms-verified-picker" class="picker hidden"></div>
233
- </div>
234
- </div>
235
- </div>
236
-
237
- <!-- ── ntfy ─────────────────────────────────────────────────── -->
238
- <div class="card" id="card-ntfy">
239
- <div class="card-hd" onclick="toggleCard('ntfy')">
240
- <div class="ch-meta">
241
- <div class="channel-icon ntfy-icon">
242
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
243
- </div>
244
- <span class="ch-name">ntfy</span><span class="tag">Push</span>
245
- </div>
246
- <span class="badge badge-idle" id="badge-ntfy">Not configured</span>
247
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
248
- </div>
249
- <div class="card-body">
250
- <div class="actions">
251
- <label class="toggle-wrap">
252
- <input type="checkbox" id="ntfy-enabled" onchange="saveNtfy()">
253
- <span class="toggle"></span>
254
- </label>
255
- <span class="toggle-lbl">Enable</span>
256
- <button class="btn btn-sm btn-ghost" onclick="testChannel('ntfy')">Test</button>
257
- </div>
258
- <details class="guide">
259
- <summary>Set up ntfy (1 min, free, fully private)</summary>
260
- <ol>
261
- <li>Install the <a href="https://ntfy.sh" target="_blank">ntfy app</a> on your phone</li>
262
- <li>In the app: tap <b>+</b> → <b>Use another server</b> → enter the Server URL shown below</li>
263
- <li>Pick a topic name and subscribe</li>
264
- <li>Paste the same topic below and save</li>
265
- </ol>
266
- </details>
267
- <div class="fg">
268
- <label>Server URL (for ntfy app)</label>
269
- <div style="display:flex;gap:6px">
270
- <input type="text" id="ntfy-server-url" placeholder="http://73.223.191.86:3737" oninput="saveNtfyDebounced()" style="flex:1;font-family:monospace;font-size:12px">
271
- <button class="btn btn-sm btn-ghost" onclick="copyNtfyUrl()">Copy</button>
272
- </div>
273
- <span class="hint">Paste into the ntfy app under "Use another server". Use your public IP if accessing remotely.</span>
274
- </div>
275
- <div class="fg"><label>Topic</label><input type="text" id="ntfy-topic" placeholder="Claude_Alerts" oninput="saveNtfyDebounced()"></div>
276
- </div>
277
- </div>
278
-
279
- <!-- ── Discord ───────────────────────────────────────────────── -->
280
- <div class="card" id="card-discord">
281
- <div class="card-hd" onclick="toggleCard('discord')">
282
- <div class="ch-meta">
283
- <div class="channel-icon discord-icon">
284
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><path d="M7.5 7.5c1-.5 2.5-.5 4.5-.5s3.5 0 4.5.5M7.5 16.5c1 .5 2.5.5 4.5.5s3.5 0 4.5-.5"/><path d="M3 3l4.5 1.5M21 3l-4.5 1.5M3 21l4.5-1.5M21 21l-4.5-1.5"/><path d="M3 3c0 9 0 12 9 15 9-3 9-6 9-15"/></svg>
285
- </div>
286
- <span class="ch-name">Discord</span><span class="tag">Webhook</span>
287
- </div>
288
- <span class="badge badge-idle" id="badge-discord">Not configured</span>
289
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
290
- </div>
291
- <div class="card-body">
292
- <div class="actions">
293
- <label class="toggle-wrap">
294
- <input type="checkbox" id="discord-enabled" onchange="saveDiscord()">
295
- <span class="toggle"></span>
296
- </label>
297
- <span class="toggle-lbl">Enable</span>
298
- <button class="btn btn-sm btn-ghost" onclick="testChannel('discord')">Test</button>
299
- </div>
300
- <div id="discord-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
301
- <!-- PRIMARY: one-click browser sign-in — pick a server+channel, webhook captured -->
302
- <div class="fg" id="discord-oauth">
303
- <a class="btn btn-primary" id="discord-connect-btn" href="/auth/discord/start" target="_blank" rel="noopener">🔗 Connect Discord (browser sign-in)</a>
304
- <button class="btn btn-sm btn-danger hidden" id="discord-disconnect-btn" onclick="disconnectDiscord()">Disconnect</button>
305
- <span class="hint">One click — authorize, pick a server + channel, the webhook is captured automatically. First time only: add your Discord app's Client ID/Secret under <b>Advanced</b>.</span>
306
- </div>
307
- <details class="guide" id="discord-advanced">
308
- <summary>Advanced / manual setup</summary>
309
- <ol>
310
- <li><b>To enable Connect:</b> <a href="https://discord.com/developers/applications" target="_blank">discord.com/developers</a> → New Application → <b>OAuth2</b> → copy <b>Client ID</b> + <b>Client Secret</b> below; add redirect <code class="cp" onclick="copyText(this)" id="discord-redirect">http://localhost:3737/auth/discord/callback</code>.</li>
311
- <li><b>Or manual webhook:</b> Discord channel → Edit Channel → Integrations → Webhooks → New Webhook → Copy URL → paste below.</li>
312
- </ol>
313
- <div class="row2" style="padding:0 11px">
314
- <div class="fg"><label>Client ID</label><input type="text" id="discord-clientid" placeholder="from OAuth2" oninput="saveDiscordDebounced()"></div>
315
- <div class="fg"><label>Client Secret</label><input type="password" id="discord-clientsecret" placeholder="from OAuth2" oninput="saveDiscordDebounced()"></div>
316
- </div>
317
- <div class="fg" style="padding:8px 11px 0"><label>Webhook URL (manual, instead of Connect)</label><input type="password" id="discord-webhook" placeholder="https://discord.com/api/webhooks/…" oninput="saveDiscordDebounced()"></div>
318
- <div class="fg" style="padding:8px 11px 10px"><label>Bot name (optional)</label><input type="text" id="discord-username" placeholder="Claude Notify" oninput="saveDiscordDebounced()"></div>
319
- </details>
320
- </div>
321
- </div>
322
-
323
- <!-- ── Slack ─────────────────────────────────────────────────── -->
324
- <div class="card" id="card-slack">
325
- <div class="card-hd" onclick="toggleCard('slack')">
326
- <div class="ch-meta">
327
- <div class="channel-icon slack-icon">
328
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"/><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"/><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"/></svg>
329
- </div>
330
- <span class="ch-name">Slack</span><span class="tag">Webhook</span>
331
- </div>
332
- <span class="badge badge-idle" id="badge-slack">Not configured</span>
333
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
334
- </div>
335
- <div class="card-body">
336
- <div class="actions">
337
- <label class="toggle-wrap">
338
- <input type="checkbox" id="slack-enabled" onchange="saveSlack()">
339
- <span class="toggle"></span>
340
- </label>
341
- <span class="toggle-lbl">Enable</span>
342
- <button class="btn btn-sm btn-ghost" onclick="testChannel('slack')">Test</button>
343
- </div>
344
- <div id="slack-token-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
345
- <!-- PRIMARY: one-click browser sign-in (the recommended, frictionless path) -->
346
- <div class="fg" id="slack-oauth">
347
- <a class="btn btn-primary" id="slack-connect-btn" href="/auth/slack/start" target="_blank" rel="noopener">🔗 Connect Slack (browser sign-in)</a>
348
- <button class="btn btn-sm btn-danger hidden" id="slack-disconnect-btn" onclick="disconnectSlack()">Disconnect</button>
349
- <span class="hint">One click — authorize in your browser, no tokens to copy. First time only: add your Slack app's Client ID/Secret under <b>Advanced</b>.</span>
350
- </div>
351
- <div class="fg">
352
- <label>Channels to notify</label>
353
- <div id="slack-chips" class="chips"></div>
354
- <div class="dest-add">
355
- <button class="btn btn-sm btn-primary" onclick="loadSlackChannels()">Load channels</button>
356
- <input type="text" id="slack-channel-manual" placeholder="or paste channel ID (C0…)" style="flex:1">
357
- <button class="btn btn-sm btn-ghost" onclick="addSlackChannelManual()">Add</button>
358
- </div>
359
- <div id="slack-channel-picker" class="picker hidden"></div>
360
- </div>
361
- <details class="guide" id="slack-advanced">
362
- <summary>Advanced / manual setup</summary>
363
- <ol>
364
- <li><b>To enable Connect:</b> <a href="https://api.slack.com/apps" target="_blank">api.slack.com/apps</a> → your app → <b>Basic Information</b> → copy <b>Client ID</b> + <b>Client Secret</b> below.</li>
365
- <li><b>OAuth &amp; Permissions → Redirect URLs</b> → Add → paste <code class="cp" onclick="copyText(this)" id="slack-redirect">http://localhost:3737/auth/slack/callback</code> → Save.</li>
366
- </ol>
367
- <div class="row2" style="padding:0 11px">
368
- <div class="fg"><label>Client ID</label><input type="text" id="slack-clientid" placeholder="from Basic Information" oninput="saveSlackDebounced()"></div>
369
- <div class="fg"><label>Client Secret</label><input type="password" id="slack-clientsecret" placeholder="from Basic Information" oninput="saveSlackDebounced()"></div>
370
- </div>
371
- <div class="fg" style="padding:8px 11px 0">
372
- <label>Bot token (manual, instead of Connect)</label>
373
- <input type="password" id="slack-bottoken" placeholder="xoxb-… (blank = reuse configured token)" oninput="saveSlackDebounced()">
374
- </div>
375
- <div class="fg" style="padding:8px 11px 10px">
376
- <label>Webhook URL (single channel, no bot token)</label>
377
- <input type="password" id="slack-webhook" placeholder="https://hooks.slack.com/services/…" oninput="saveSlackDebounced()">
378
- </div>
379
- </details>
380
- </div>
381
- </div>
382
-
383
- <!-- ── Teams ─────────────────────────────────────────────────── -->
384
- <div class="card" id="card-teams">
385
- <div class="card-hd" onclick="toggleCard('teams')">
386
- <div class="ch-meta">
387
- <div class="channel-icon teams-icon">
388
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
389
- </div>
390
- <span class="ch-name">Teams</span><span class="tag">Webhook</span>
391
- </div>
392
- <span class="badge badge-idle" id="badge-teams">Not configured</span>
393
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
394
- </div>
395
- <div class="card-body">
396
- <div class="actions">
397
- <label class="toggle-wrap">
398
- <input type="checkbox" id="teams-enabled" onchange="saveTeams()">
399
- <span class="toggle"></span>
400
- </label>
401
- <span class="toggle-lbl">Enable</span>
402
- <button class="btn btn-sm btn-ghost" onclick="testChannel('teams')">Test</button>
403
- </div>
404
- <details class="guide">
405
- <summary>Create a Teams webhook (2 min)</summary>
406
- <ol>
407
- <li>In Teams, go to the channel → ••• → Workflows → "Post to a channel when a webhook request is received"</li>
408
- <li>Follow the wizard → copy the webhook URL at the end</li>
409
- </ol>
410
- </details>
411
- <div class="fg"><label>Webhook URL</label><input type="password" id="teams-webhook" placeholder="https://…webhook.office.com/…" oninput="saveTeamsDebounced()"></div>
412
- </div>
413
- </div>
414
-
415
- </div>
416
- </section>
417
-
418
- <section class="section section-policies">
419
- <div class="section-hd">
420
- <span class="section-hd-title">System policies</span>
421
- <span class="section-hd-sub">When &amp; how to suppress</span>
422
- </div>
423
- <div class="card-list" id="card-list-policies">
424
-
425
- <!-- ── Do Not Disturb ────────────────────────────────────────── -->
426
- <div class="card" id="card-dnd">
427
- <div class="card-hd" onclick="toggleCard('dnd')">
428
- <div class="ch-meta">
429
- <div class="channel-icon" style="background:#3b2f12;color:#f59e0b">
430
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg>
431
- </div>
432
- <span class="ch-name">Do Not Disturb</span>
433
- </div>
434
- <span class="badge badge-idle" id="badge-dnd">–</span>
435
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
436
- </div>
437
- <div class="card-body">
438
- <div class="actions">
439
- <label class="toggle-wrap">
440
- <input type="checkbox" id="dnd-enabled" onchange="saveDnd()">
441
- <span class="toggle"></span>
442
- </label>
443
- <span class="toggle-lbl">DND on (manual)</span>
444
- </div>
445
- <p class="hint">When DND is on, the server suppresses delivery for <code>priority &lt; high</code>. <code>priority='high'</code> always fires.</p>
446
- <div class="actions" style="margin-top:8px">
447
- <label class="toggle-wrap">
448
- <input type="checkbox" id="dnd-schedule-enabled" onchange="saveDnd()">
449
- <span class="toggle"></span>
450
- </label>
451
- <span class="toggle-lbl">Scheduled DND</span>
452
- </div>
453
- <div class="row2">
454
- <div class="fg"><label>Quiet start</label><input type="time" id="dnd-quiet-start" value="22:00" onchange="saveDnd()"></div>
455
- <div class="fg"><label>Quiet end</label><input type="time" id="dnd-quiet-end" value="08:00" onchange="saveDnd()"></div>
456
- </div>
457
- <div class="fg" style="margin-top:8px">
458
- <label>Active on days</label>
459
- <div id="dnd-days" style="display:flex;gap:8px;flex-wrap:wrap">
460
- <label><input type="checkbox" data-day="0" onchange="saveDnd()"> Sun</label>
461
- <label><input type="checkbox" data-day="1" onchange="saveDnd()"> Mon</label>
462
- <label><input type="checkbox" data-day="2" onchange="saveDnd()"> Tue</label>
463
- <label><input type="checkbox" data-day="3" onchange="saveDnd()"> Wed</label>
464
- <label><input type="checkbox" data-day="4" onchange="saveDnd()"> Thu</label>
465
- <label><input type="checkbox" data-day="5" onchange="saveDnd()"> Fri</label>
466
- <label><input type="checkbox" data-day="6" onchange="saveDnd()"> Sat</label>
467
- </div>
468
- </div>
469
- </div>
470
- </div>
471
-
472
- <!-- ── Idle gating ───────────────────────────────────────────── -->
473
- <div class="card" id="card-idle">
474
- <div class="card-hd" onclick="toggleCard('idle')">
475
- <div class="ch-meta">
476
- <div class="channel-icon" style="background:#12293b;color:#3b82f6">
477
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
478
- </div>
479
- <span class="ch-name">Idle gating</span>
480
- </div>
481
- <span class="badge badge-idle" id="badge-idle">–</span>
482
- <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
483
- </div>
484
- <div class="card-body">
485
- <div class="actions">
486
- <label class="toggle-wrap">
487
- <input type="checkbox" id="idle-enabled" onchange="saveIdle()">
488
- <span class="toggle"></span>
489
- </label>
490
- <span class="toggle-lbl">Gate non-urgent notifs when user is active</span>
491
- </div>
492
- <p class="hint">When you're at the keyboard, the server suppresses remote channels (Telegram/SMS/email) for non-urgent notifs. <code>priority='high'</code> always fires.</p>
493
- <div class="fg">
494
- <label>Threshold (seconds)</label>
495
- <input type="number" id="idle-threshold" min="10" max="3600" value="120" oninput="saveIdleDebounced()">
496
- <p class="hint">Default 120s (2 min). Under this, you're considered active.</p>
497
- </div>
498
- <div class="actions" style="margin-top:8px">
499
- <label class="toggle-wrap">
500
- <input type="checkbox" id="idle-always-desktop" onchange="saveIdle()">
501
- <span class="toggle"></span>
502
- </label>
503
- <span class="toggle-lbl">Always play desktop sound when active</span>
504
- </div>
505
- <p class="hint">Recommended ON. Lets you know <em>something</em> happened without blasting your phone — handy when running multiple agents.</p>
506
- </div>
507
- </div>
508
-
509
- </div>
510
- </section>
511
- </div>
512
-
513
- <div class="log-section collapsed" id="log-section">
514
- <div class="log-header">
515
- <button class="log-collapse" onclick="toggleLog()" title="Collapse / expand">
516
- <svg class="log-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
517
- </button>
518
- <div class="log-tabs">
519
- <button class="log-tab log-tab-active" data-ptab="log" onclick="selectPanelTab('log')">
520
- <span class="dot dot-ok" id="log-dot" style="display:inline-block"></span>
521
- Activity Log
522
- </button>
523
- <button class="log-tab" data-ptab="clients" onclick="selectPanelTab('clients')">Clients</button>
524
- </div>
525
- <div class="session-pills" id="session-pills">
526
- <button class="pill pill-active" data-client="" onclick="selectLogFilter('')">All</button>
527
- </div>
528
- <button class="btn btn-sm btn-ghost" id="clear-log-btn" onclick="clearLog()">Clear</button>
529
- </div>
530
- <div class="log-panel" id="log-panel"></div>
531
- <div class="clients-panel" id="clients-panel" style="display:none"></div>
532
- </div>
533
-
534
- </main>
535
-
536
- <script src="app.js"></script>
537
- </body>
538
- </html>
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 Notify</title>
7
+ <link rel="icon" href="assets/logo.svg" type="image/svg+xml">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div id="toast" class="toast hidden"></div>
12
+
13
+ <header>
14
+ <div class="logo">
15
+ <img src="assets/logo.svg" width="22" height="22" alt="">
16
+ omni-notify-mcp
17
+ </div>
18
+ <span class="subtitle">Multi-channel notifications for AI agents</span>
19
+ <a href="/help.html" class="header-link" style="margin-left:auto">Help &amp; setup →</a>
20
+ </header>
21
+
22
+ <main>
23
+ <div class="mute-all-bar" id="mute-all-bar">
24
+ <div class="mute-all-main">
25
+ <span class="mute-all-icon">🔕</span>
26
+ <div class="mute-all-text">
27
+ <span class="mute-all-title">Disable all notifications</span>
28
+ <span class="mute-all-sub" id="mute-all-sub">Master kill switch — silences every channel &amp; client, including high-priority. Overrides DND.</span>
29
+ </div>
30
+ </div>
31
+ <label class="toggle-wrap">
32
+ <input type="checkbox" id="mute-all" onchange="saveMuteAll()">
33
+ <span class="toggle"></span>
34
+ </label>
35
+ </div>
36
+ <div class="config-panel">
37
+ <section class="section section-channels">
38
+ <div class="section-hd">
39
+ <span class="section-hd-title">Delivery channels</span>
40
+ <span class="section-hd-sub">Where notifications go</span>
41
+ </div>
42
+ <div class="card-list" id="card-list-channels">
43
+
44
+ <!-- ── Desktop ───────────────────────────────────────────────── -->
45
+ <div class="card" id="card-desktop">
46
+ <div class="card-hd" onclick="toggleCard('desktop')">
47
+ <div class="ch-meta">
48
+ <div class="channel-icon desktop-icon">
49
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
50
+ </div>
51
+ <span class="ch-name">Desktop</span>
52
+ </div>
53
+ <span class="badge badge-idle" id="badge-desktop">–</span>
54
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
55
+ </div>
56
+ <div class="card-body">
57
+ <div class="actions">
58
+ <label class="toggle-wrap">
59
+ <input type="checkbox" id="desktop-enabled" onchange="saveDesktop()">
60
+ <span class="toggle"></span>
61
+ </label>
62
+ <span class="toggle-lbl">Enable</span>
63
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('desktop')">Test</button>
64
+ </div>
65
+ <div class="actions" style="margin-top:6px">
66
+ <label class="toggle-wrap">
67
+ <input type="checkbox" id="desktop-sound" onchange="saveDesktop()">
68
+ <span class="toggle"></span>
69
+ </label>
70
+ <span class="toggle-lbl">Play system sound</span>
71
+ <button class="btn btn-sm btn-ghost" onclick="testSound()">Test sound</button>
72
+ </div>
73
+ <div class="actions" style="margin-top:6px">
74
+ <label class="toggle-wrap">
75
+ <input type="checkbox" id="desktop-tts" onchange="saveDesktop()">
76
+ <span class="toggle"></span>
77
+ </label>
78
+ <span class="toggle-lbl">Speak notification</span>
79
+ <button class="btn btn-sm btn-ghost" onclick="testTts()">Test voice</button>
80
+ </div>
81
+ <div id="tts-voice-row" class="fg" style="margin-top:8px; display:none">
82
+ <label>Voice</label>
83
+ <select id="desktop-tts-voice" onchange="saveDesktop()"></select>
84
+ </div>
85
+ <span id="os-hint" class="os-tag hidden"></span>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- ── Email ─────────────────────────────────────────────────── -->
90
+ <div class="card" id="card-email">
91
+ <div class="card-hd" onclick="toggleCard('email')">
92
+ <div class="ch-meta">
93
+ <div class="channel-icon gmail-icon">
94
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
95
+ </div>
96
+ <span class="ch-name">Email</span><span class="tag">Gmail</span>
97
+ </div>
98
+ <span class="badge badge-idle" id="badge-email">Not configured</span>
99
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
100
+ </div>
101
+ <div class="card-body">
102
+ <!-- Connected -->
103
+ <div id="gmail-connected-state" class="hidden">
104
+ <div class="actions">
105
+ <label class="toggle-wrap">
106
+ <input type="checkbox" id="email-enabled" onchange="saveEmail()">
107
+ <span class="toggle"></span>
108
+ </label>
109
+ <span class="toggle-lbl">Enable</span>
110
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('email')">Test</button>
111
+ </div>
112
+ <div class="ok-banner">
113
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
114
+ <strong id="gmail-connected-email"></strong>
115
+ </div>
116
+ <div class="fg"><label>Send to</label><input type="email" id="gmail-to-connected" placeholder="you@gmail.com" oninput="saveEmailDebounced()"></div>
117
+ </div>
118
+ <!-- Setup -->
119
+ <div id="gmail-setup-state">
120
+ <div class="actions">
121
+ <button class="btn btn-sm btn-ghost" onclick="openAppPasswords()">Open Google Account</button>
122
+ <button class="btn btn-sm btn-primary" onclick="saveAppPassword()">Connect</button>
123
+ </div>
124
+ <details class="guide" id="gmail-guide">
125
+ <summary>How to get an App Password</summary>
126
+ <ol>
127
+ <li>Click <b>Open Google Account</b> above (needs 2FA enabled)</li>
128
+ <li>Select app: <b>Mail</b>, device: <b>Mac</b> → Generate</li>
129
+ <li>Copy the 16-char password and paste below</li>
130
+ </ol>
131
+ </details>
132
+ <div class="fg"><label>Gmail address</label><input type="email" id="gmail-address" placeholder="you@gmail.com"></div>
133
+ <div class="fg"><label>App Password (16 chars)</label><input type="password" id="gmail-app-password" placeholder="xxxx xxxx xxxx xxxx"></div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- ── Telegram ──────────────────────────────────────────────── -->
139
+ <div class="card" id="card-telegram">
140
+ <div class="card-hd" onclick="toggleCard('telegram')">
141
+ <div class="ch-meta">
142
+ <div class="channel-icon telegram-icon">
143
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
144
+ </div>
145
+ <span class="ch-name">Telegram</span><span class="tag">Bot API</span>
146
+ </div>
147
+ <span class="badge badge-idle" id="badge-telegram">Not configured</span>
148
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
149
+ </div>
150
+ <div class="card-body">
151
+ <div class="actions">
152
+ <label class="toggle-wrap">
153
+ <input type="checkbox" id="telegram-enabled" onchange="saveTelegram()">
154
+ <span class="toggle"></span>
155
+ </label>
156
+ <span class="toggle-lbl">Enable</span>
157
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('telegram')">Test</button>
158
+ </div>
159
+ <details class="guide">
160
+ <summary>Create a bot (2 min)</summary>
161
+ <ol>
162
+ <li>Open Telegram → search <b>@BotFather</b></li>
163
+ <li>Send <code class="cp" onclick="copyText(this)">/newbot</code> → follow prompts → copy token</li>
164
+ <li>From each chat/group you want notified, send your bot any message (add it to groups first)</li>
165
+ <li>Paste token below → click <b>Detect chats</b> → tick the chats</li>
166
+ </ol>
167
+ </details>
168
+ <div class="fg"><label>Bot Token</label><input type="password" id="telegram-token" placeholder="123456:ABC-DEF…" oninput="saveTelegramDebounced()"></div>
169
+ <div class="fg">
170
+ <label>Chats to notify</label>
171
+ <div id="telegram-chips" class="chips"></div>
172
+ <div class="dest-add">
173
+ <button class="btn btn-sm btn-primary" onclick="detectTelegramChats()">Detect chats</button>
174
+ <input type="text" id="telegram-chat-manual" placeholder="or paste chat ID" style="flex:1">
175
+ <button class="btn btn-sm btn-ghost" onclick="addTelegramChatManual()">Add</button>
176
+ </div>
177
+ <div id="telegram-chat-picker" class="picker hidden"></div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- ── SMS ───────────────────────────────────────────────────── -->
183
+ <div class="card" id="card-sms">
184
+ <div class="card-hd" onclick="toggleCard('sms')">
185
+ <div class="ch-meta">
186
+ <div class="channel-icon sms-icon">
187
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
188
+ </div>
189
+ <span class="ch-name">SMS</span><span class="tag">AWS</span>
190
+ </div>
191
+ <span class="badge badge-idle" id="badge-sms">Not configured</span>
192
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
193
+ </div>
194
+ <div class="card-body">
195
+ <div class="actions">
196
+ <label class="toggle-wrap">
197
+ <input type="checkbox" id="sms-enabled" onchange="saveSms()">
198
+ <span class="toggle"></span>
199
+ </label>
200
+ <span class="toggle-lbl">Enable</span>
201
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('sms')">Test</button>
202
+ </div>
203
+ <div id="sms-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
204
+ <details class="guide">
205
+ <summary>AWS End User Messaging credentials</summary>
206
+ <ol>
207
+ <li>AWS Console → IAM user with <code class="cp" onclick="copyText(this)">sms-voice:SendTextMessage</code> → copy <b>Access Key ID</b> + <b>Secret</b></li>
208
+ <li>AWS End User Messaging → Phone numbers → request a number → it becomes your origination number</li>
209
+ <li>Paste below → <b>Discover</b> to auto-fill your numbers → add recipients</li>
210
+ </ol>
211
+ </details>
212
+ <div class="row2">
213
+ <div class="fg"><label>Access Key ID</label><input type="text" id="sms-accesskey" placeholder="AKIA…" oninput="saveSmsDebounced()"></div>
214
+ <div class="fg"><label>Secret Access Key</label><input type="password" id="sms-secret" placeholder="••••••••" oninput="saveSmsDebounced()"></div>
215
+ <div class="fg"><label>Region</label><input type="text" id="sms-region" placeholder="us-east-1" oninput="saveSmsDebounced()"></div>
216
+ <div class="fg">
217
+ <label>From (origination number)</label>
218
+ <div class="dest-add">
219
+ <input type="text" id="sms-from" list="sms-from-list" placeholder="+1… or auto" oninput="saveSmsDebounced()" style="flex:1">
220
+ <datalist id="sms-from-list"></datalist>
221
+ <button class="btn btn-sm btn-ghost" onclick="discoverSmsNumbers()">Discover</button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ <div class="fg">
226
+ <label>Send to (recipients)</label>
227
+ <div id="sms-chips" class="chips"></div>
228
+ <div class="dest-add">
229
+ <input type="tel" id="sms-to-manual" placeholder="+1 234 567 8900" style="flex:1">
230
+ <button class="btn btn-sm btn-ghost" onclick="addSmsNumberManual()">Add</button>
231
+ </div>
232
+ <div id="sms-verified-picker" class="picker hidden"></div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <!-- ── ntfy ─────────────────────────────────────────────────── -->
238
+ <div class="card" id="card-ntfy">
239
+ <div class="card-hd" onclick="toggleCard('ntfy')">
240
+ <div class="ch-meta">
241
+ <div class="channel-icon ntfy-icon">
242
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
243
+ </div>
244
+ <span class="ch-name">ntfy</span><span class="tag">Push</span>
245
+ </div>
246
+ <span class="badge badge-idle" id="badge-ntfy">Not configured</span>
247
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
248
+ </div>
249
+ <div class="card-body">
250
+ <div class="actions">
251
+ <label class="toggle-wrap">
252
+ <input type="checkbox" id="ntfy-enabled" onchange="saveNtfy()">
253
+ <span class="toggle"></span>
254
+ </label>
255
+ <span class="toggle-lbl">Enable</span>
256
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('ntfy')">Test</button>
257
+ </div>
258
+ <details class="guide">
259
+ <summary>Set up ntfy (1 min, free, fully private)</summary>
260
+ <ol>
261
+ <li>Install the <a href="https://ntfy.sh" target="_blank">ntfy app</a> on your phone</li>
262
+ <li>In the app: tap <b>+</b> → <b>Use another server</b> → enter the Server URL shown below</li>
263
+ <li>Pick a topic name and subscribe</li>
264
+ <li>Paste the same topic below and save</li>
265
+ </ol>
266
+ </details>
267
+ <div class="fg">
268
+ <label>Server URL (for ntfy app)</label>
269
+ <div style="display:flex;gap:6px">
270
+ <input type="text" id="ntfy-server-url" placeholder="http://73.223.191.86:3737" oninput="saveNtfyDebounced()" style="flex:1;font-family:monospace;font-size:12px">
271
+ <button class="btn btn-sm btn-ghost" onclick="copyNtfyUrl()">Copy</button>
272
+ </div>
273
+ <span class="hint">Paste into the ntfy app under "Use another server". Use your public IP if accessing remotely.</span>
274
+ </div>
275
+ <div class="fg"><label>Topic</label><input type="text" id="ntfy-topic" placeholder="Claude_Alerts" oninput="saveNtfyDebounced()"></div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- ── Discord ───────────────────────────────────────────────── -->
280
+ <div class="card" id="card-discord">
281
+ <div class="card-hd" onclick="toggleCard('discord')">
282
+ <div class="ch-meta">
283
+ <div class="channel-icon discord-icon">
284
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><path d="M7.5 7.5c1-.5 2.5-.5 4.5-.5s3.5 0 4.5.5M7.5 16.5c1 .5 2.5.5 4.5.5s3.5 0 4.5-.5"/><path d="M3 3l4.5 1.5M21 3l-4.5 1.5M3 21l4.5-1.5M21 21l-4.5-1.5"/><path d="M3 3c0 9 0 12 9 15 9-3 9-6 9-15"/></svg>
285
+ </div>
286
+ <span class="ch-name">Discord</span><span class="tag">Webhook</span>
287
+ </div>
288
+ <span class="badge badge-idle" id="badge-discord">Not configured</span>
289
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
290
+ </div>
291
+ <div class="card-body">
292
+ <div class="actions">
293
+ <label class="toggle-wrap">
294
+ <input type="checkbox" id="discord-enabled" onchange="saveDiscord()">
295
+ <span class="toggle"></span>
296
+ </label>
297
+ <span class="toggle-lbl">Enable</span>
298
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('discord')">Test</button>
299
+ </div>
300
+ <div id="discord-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
301
+ <!-- PRIMARY: one-click browser sign-in — pick a server+channel, webhook captured -->
302
+ <div class="fg" id="discord-oauth">
303
+ <a class="btn btn-primary" id="discord-connect-btn" href="/auth/discord/start" target="_blank" rel="noopener">🔗 Connect Discord (browser sign-in)</a>
304
+ <button class="btn btn-sm btn-danger hidden" id="discord-disconnect-btn" onclick="disconnectDiscord()">Disconnect</button>
305
+ <span class="hint">One click — authorize, pick a server + channel, the webhook is captured automatically. First time only: add your Discord app's Client ID/Secret under <b>Advanced</b>.</span>
306
+ </div>
307
+ <details class="guide" id="discord-advanced">
308
+ <summary>Advanced / manual setup</summary>
309
+ <ol>
310
+ <li><b>To enable Connect:</b> <a href="https://discord.com/developers/applications" target="_blank">discord.com/developers</a> → New Application → <b>OAuth2</b> → copy <b>Client ID</b> + <b>Client Secret</b> below; add redirect <code class="cp" onclick="copyText(this)" id="discord-redirect">http://localhost:3737/auth/discord/callback</code>.</li>
311
+ <li><b>Or manual webhook:</b> Discord channel → Edit Channel → Integrations → Webhooks → New Webhook → Copy URL → paste below.</li>
312
+ </ol>
313
+ <div class="row2" style="padding:0 11px">
314
+ <div class="fg"><label>Client ID</label><input type="text" id="discord-clientid" placeholder="from OAuth2" oninput="saveDiscordDebounced()"></div>
315
+ <div class="fg"><label>Client Secret</label><input type="password" id="discord-clientsecret" placeholder="from OAuth2" oninput="saveDiscordDebounced()"></div>
316
+ </div>
317
+ <div class="fg" style="padding:8px 11px 0"><label>Webhook URL (manual, instead of Connect)</label><input type="password" id="discord-webhook" placeholder="https://discord.com/api/webhooks/…" oninput="saveDiscordDebounced()"></div>
318
+ <div class="fg" style="padding:8px 11px 10px"><label>Bot name (optional)</label><input type="text" id="discord-username" placeholder="Claude Notify" oninput="saveDiscordDebounced()"></div>
319
+ </details>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- ── Slack ─────────────────────────────────────────────────── -->
324
+ <div class="card" id="card-slack">
325
+ <div class="card-hd" onclick="toggleCard('slack')">
326
+ <div class="ch-meta">
327
+ <div class="channel-icon slack-icon">
328
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"/><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"/><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"/></svg>
329
+ </div>
330
+ <span class="ch-name">Slack</span><span class="tag">Webhook</span>
331
+ </div>
332
+ <span class="badge badge-idle" id="badge-slack">Not configured</span>
333
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
334
+ </div>
335
+ <div class="card-body">
336
+ <div class="actions">
337
+ <label class="toggle-wrap">
338
+ <input type="checkbox" id="slack-enabled" onchange="saveSlack()">
339
+ <span class="toggle"></span>
340
+ </label>
341
+ <span class="toggle-lbl">Enable</span>
342
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('slack')">Test</button>
343
+ </div>
344
+ <div id="slack-token-status" class="ok-banner hidden" style="margin-bottom:4px"></div>
345
+ <!-- PRIMARY: one-click browser sign-in (the recommended, frictionless path) -->
346
+ <div class="fg" id="slack-oauth">
347
+ <a class="btn btn-primary" id="slack-connect-btn" href="/auth/slack/start" target="_blank" rel="noopener">🔗 Connect Slack (browser sign-in)</a>
348
+ <button class="btn btn-sm btn-danger hidden" id="slack-disconnect-btn" onclick="disconnectSlack()">Disconnect</button>
349
+ <span class="hint">One click — authorize in your browser, no tokens to copy. First time only: add your Slack app's Client ID/Secret under <b>Advanced</b>.</span>
350
+ </div>
351
+ <div class="fg">
352
+ <label>Channels to notify</label>
353
+ <div id="slack-chips" class="chips"></div>
354
+ <div class="dest-add">
355
+ <button class="btn btn-sm btn-primary" onclick="loadSlackChannels()">Load channels</button>
356
+ <input type="text" id="slack-channel-manual" placeholder="or paste channel ID (C0…)" style="flex:1">
357
+ <button class="btn btn-sm btn-ghost" onclick="addSlackChannelManual()">Add</button>
358
+ </div>
359
+ <div id="slack-channel-picker" class="picker hidden"></div>
360
+ </div>
361
+ <details class="guide" id="slack-advanced">
362
+ <summary>Advanced / manual setup</summary>
363
+ <ol>
364
+ <li><b>To enable Connect:</b> <a href="https://api.slack.com/apps" target="_blank">api.slack.com/apps</a> → your app → <b>Basic Information</b> → copy <b>Client ID</b> + <b>Client Secret</b> below.</li>
365
+ <li><b>OAuth &amp; Permissions → Redirect URLs</b> → Add → paste <code class="cp" onclick="copyText(this)" id="slack-redirect">http://localhost:3737/auth/slack/callback</code> → Save.</li>
366
+ </ol>
367
+ <div class="row2" style="padding:0 11px">
368
+ <div class="fg"><label>Client ID</label><input type="text" id="slack-clientid" placeholder="from Basic Information" oninput="saveSlackDebounced()"></div>
369
+ <div class="fg"><label>Client Secret</label><input type="password" id="slack-clientsecret" placeholder="from Basic Information" oninput="saveSlackDebounced()"></div>
370
+ </div>
371
+ <div class="fg" style="padding:8px 11px 0">
372
+ <label>Bot token (manual, instead of Connect)</label>
373
+ <input type="password" id="slack-bottoken" placeholder="xoxb-… (blank = reuse configured token)" oninput="saveSlackDebounced()">
374
+ </div>
375
+ <div class="fg" style="padding:8px 11px 10px">
376
+ <label>Webhook URL (single channel, no bot token)</label>
377
+ <input type="password" id="slack-webhook" placeholder="https://hooks.slack.com/services/…" oninput="saveSlackDebounced()">
378
+ </div>
379
+ </details>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- ── Teams ─────────────────────────────────────────────────── -->
384
+ <div class="card" id="card-teams">
385
+ <div class="card-hd" onclick="toggleCard('teams')">
386
+ <div class="ch-meta">
387
+ <div class="channel-icon teams-icon">
388
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
389
+ </div>
390
+ <span class="ch-name">Teams</span><span class="tag">Webhook</span>
391
+ </div>
392
+ <span class="badge badge-idle" id="badge-teams">Not configured</span>
393
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
394
+ </div>
395
+ <div class="card-body">
396
+ <div class="actions">
397
+ <label class="toggle-wrap">
398
+ <input type="checkbox" id="teams-enabled" onchange="saveTeams()">
399
+ <span class="toggle"></span>
400
+ </label>
401
+ <span class="toggle-lbl">Enable</span>
402
+ <button class="btn btn-sm btn-ghost" onclick="testChannel('teams')">Test</button>
403
+ </div>
404
+ <details class="guide">
405
+ <summary>Create a Teams webhook (2 min)</summary>
406
+ <ol>
407
+ <li>In Teams, go to the channel → ••• → Workflows → "Post to a channel when a webhook request is received"</li>
408
+ <li>Follow the wizard → copy the webhook URL at the end</li>
409
+ </ol>
410
+ </details>
411
+ <div class="fg"><label>Webhook URL</label><input type="password" id="teams-webhook" placeholder="https://…webhook.office.com/…" oninput="saveTeamsDebounced()"></div>
412
+ </div>
413
+ </div>
414
+
415
+ </div>
416
+ </section>
417
+
418
+ <section class="section section-policies">
419
+ <div class="section-hd">
420
+ <span class="section-hd-title">System policies</span>
421
+ <span class="section-hd-sub">When &amp; how to suppress</span>
422
+ </div>
423
+ <div class="card-list" id="card-list-policies">
424
+
425
+ <!-- ── Do Not Disturb ────────────────────────────────────────── -->
426
+ <div class="card" id="card-dnd">
427
+ <div class="card-hd" onclick="toggleCard('dnd')">
428
+ <div class="ch-meta">
429
+ <div class="channel-icon" style="background:#3b2f12;color:#f59e0b">
430
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M4.93 4.93l14.14 14.14"/></svg>
431
+ </div>
432
+ <span class="ch-name">Do Not Disturb</span>
433
+ </div>
434
+ <span class="badge badge-idle" id="badge-dnd">–</span>
435
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
436
+ </div>
437
+ <div class="card-body">
438
+ <div class="actions">
439
+ <label class="toggle-wrap">
440
+ <input type="checkbox" id="dnd-enabled" onchange="saveDnd()">
441
+ <span class="toggle"></span>
442
+ </label>
443
+ <span class="toggle-lbl">DND on (manual)</span>
444
+ </div>
445
+ <p class="hint">When DND is on, the server suppresses delivery for <code>priority &lt; high</code>. <code>priority='high'</code> always fires.</p>
446
+ <div class="actions" style="margin-top:8px">
447
+ <label class="toggle-wrap">
448
+ <input type="checkbox" id="dnd-schedule-enabled" onchange="saveDnd()">
449
+ <span class="toggle"></span>
450
+ </label>
451
+ <span class="toggle-lbl">Scheduled DND</span>
452
+ </div>
453
+ <div class="row2">
454
+ <div class="fg"><label>Quiet start</label><input type="time" id="dnd-quiet-start" value="22:00" onchange="saveDnd()"></div>
455
+ <div class="fg"><label>Quiet end</label><input type="time" id="dnd-quiet-end" value="08:00" onchange="saveDnd()"></div>
456
+ </div>
457
+ <div class="fg" style="margin-top:8px">
458
+ <label>Active on days</label>
459
+ <div id="dnd-days" style="display:flex;gap:8px;flex-wrap:wrap">
460
+ <label><input type="checkbox" data-day="0" onchange="saveDnd()"> Sun</label>
461
+ <label><input type="checkbox" data-day="1" onchange="saveDnd()"> Mon</label>
462
+ <label><input type="checkbox" data-day="2" onchange="saveDnd()"> Tue</label>
463
+ <label><input type="checkbox" data-day="3" onchange="saveDnd()"> Wed</label>
464
+ <label><input type="checkbox" data-day="4" onchange="saveDnd()"> Thu</label>
465
+ <label><input type="checkbox" data-day="5" onchange="saveDnd()"> Fri</label>
466
+ <label><input type="checkbox" data-day="6" onchange="saveDnd()"> Sat</label>
467
+ </div>
468
+ </div>
469
+ </div>
470
+ </div>
471
+
472
+ <!-- ── Idle gating ───────────────────────────────────────────── -->
473
+ <div class="card" id="card-idle">
474
+ <div class="card-hd" onclick="toggleCard('idle')">
475
+ <div class="ch-meta">
476
+ <div class="channel-icon" style="background:#12293b;color:#3b82f6">
477
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
478
+ </div>
479
+ <span class="ch-name">Idle gating</span>
480
+ </div>
481
+ <span class="badge badge-idle" id="badge-idle">–</span>
482
+ <svg class="card-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
483
+ </div>
484
+ <div class="card-body">
485
+ <div class="actions">
486
+ <label class="toggle-wrap">
487
+ <input type="checkbox" id="idle-enabled" onchange="saveIdle()">
488
+ <span class="toggle"></span>
489
+ </label>
490
+ <span class="toggle-lbl">Gate non-urgent notifs when user is active</span>
491
+ </div>
492
+ <p class="hint">When you're at the keyboard, the server suppresses remote channels (Telegram/SMS/email) for non-urgent notifs. <code>priority='high'</code> always fires.</p>
493
+ <div class="fg">
494
+ <label>Threshold (seconds)</label>
495
+ <input type="number" id="idle-threshold" min="10" max="3600" value="120" oninput="saveIdleDebounced()">
496
+ <p class="hint">Default 120s (2 min). Under this, you're considered active.</p>
497
+ </div>
498
+ <div class="actions" style="margin-top:8px">
499
+ <label class="toggle-wrap">
500
+ <input type="checkbox" id="idle-always-desktop" onchange="saveIdle()">
501
+ <span class="toggle"></span>
502
+ </label>
503
+ <span class="toggle-lbl">Always play desktop sound when active</span>
504
+ </div>
505
+ <p class="hint">Recommended ON. Lets you know <em>something</em> happened without blasting your phone — handy when running multiple agents.</p>
506
+ </div>
507
+ </div>
508
+
509
+ </div>
510
+ </section>
511
+ </div>
512
+
513
+ <div class="log-section collapsed" id="log-section">
514
+ <div class="log-header">
515
+ <button class="log-collapse" onclick="toggleLog()" title="Collapse / expand">
516
+ <svg class="log-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
517
+ </button>
518
+ <div class="log-tabs">
519
+ <button class="log-tab log-tab-active" data-ptab="log" onclick="selectPanelTab('log')">
520
+ <span class="dot dot-ok" id="log-dot" style="display:inline-block"></span>
521
+ Activity Log
522
+ </button>
523
+ <button class="log-tab" data-ptab="clients" onclick="selectPanelTab('clients')">Clients</button>
524
+ </div>
525
+ <div class="session-pills" id="session-pills">
526
+ <button class="pill pill-active" data-client="" onclick="selectLogFilter('')">All</button>
527
+ </div>
528
+ <button class="btn btn-sm btn-ghost" id="clear-log-btn" onclick="clearLog()">Clear</button>
529
+ </div>
530
+ <div class="log-panel" id="log-panel"></div>
531
+ <div class="clients-panel" id="clients-panel" style="display:none"></div>
532
+ </div>
533
+
534
+ </main>
535
+
536
+ <script src="app.js"></script>
537
+ </body>
538
+ </html>