@delt/claude-alarm 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -650,24 +650,64 @@
650
650
  </div>
651
651
  </div>
652
652
  <div id="settingsTelegram" style="display:none">
653
- <p style="margin-bottom:12px;font-size:12px;color:var(--text-dim)">Connect a Telegram bot to receive notifications and send messages.</p>
654
- <div style="margin-bottom:12px">
655
- <label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Bot Token</label>
656
- <input type="text" id="tgBotToken" placeholder="123456:ABC-DEF..." autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
653
+ <!-- Step 1: Bot Token -->
654
+ <div id="tgStep1">
655
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
656
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">1</span>
657
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Create a bot &amp; paste the token</span>
658
+ </div>
659
+ <p style="margin:0 0 10px 30px;font-size:12px;color:var(--text-dim)">Open <strong>@BotFather</strong> on Telegram → <code>/newbot</code> → copy the token</p>
660
+ <div style="margin-left:30px;margin-bottom:12px">
661
+ <input type="text" id="tgBotToken" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v..." autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
662
+ </div>
663
+ <div style="margin-left:30px">
664
+ <button id="tgNextStep2" style="padding:8px 20px;font-size:13px">Next →</button>
665
+ </div>
657
666
  </div>
658
- <div style="margin-bottom:12px">
659
- <label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Chat ID</label>
660
- <input type="text" id="tgChatId" placeholder="-1001234567890" autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
661
- </div>
662
- <div style="margin-bottom:16px;display:flex;align-items:center;gap:8px">
663
- <input type="checkbox" id="tgEnabled" style="width:16px;height:16px;accent-color:var(--accent)">
664
- <label for="tgEnabled" style="font-size:13px;color:var(--text)">Enable Telegram notifications</label>
667
+
668
+ <!-- Step 2: Detect Chat ID -->
669
+ <div id="tgStep2" style="display:none">
670
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
671
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">2</span>
672
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Find your Chat ID</span>
673
+ </div>
674
+ <p style="margin:0 0 10px 30px;font-size:12px;color:var(--text-dim)">Send any message to your bot on Telegram, then click Detect.</p>
675
+ <div style="margin-left:30px;margin-bottom:10px;display:flex;gap:8px">
676
+ <button id="tgDetect" style="padding:8px 20px;font-size:13px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-weight:500">Detect Chat ID</button>
677
+ <span id="tgDetectStatus" style="font-size:12px;color:var(--text-dim);display:flex;align-items:center"></span>
678
+ </div>
679
+ <div id="tgChatList" style="margin-left:30px;margin-bottom:10px"></div>
680
+ <div style="margin-left:30px;margin-bottom:12px">
681
+ <label style="display:block;font-size:11px;color:var(--text-dim);margin-bottom:4px">Or enter manually:</label>
682
+ <input type="text" id="tgChatId" placeholder="-1001234567890" autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
683
+ </div>
684
+ <div style="margin-left:30px;display:flex;gap:8px">
685
+ <button id="tgBackStep1" style="padding:8px 20px;font-size:13px;background:none;color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer">← Back</button>
686
+ <button id="tgNextStep3" style="padding:8px 20px;font-size:13px">Next →</button>
687
+ </div>
665
688
  </div>
666
- <div id="tgStatus" style="display:none;margin-bottom:12px;padding:8px 12px;border-radius:6px;font-size:12px"></div>
667
- <div style="display:flex;gap:8px">
668
- <button id="tgTest" style="flex:1;padding:10px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-size:13px;font-weight:500">Test</button>
669
- <button id="tgSave" style="flex:1;padding:10px">Save</button>
670
- <button id="settingsCancel2" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
689
+
690
+ <!-- Step 3: Test & Save -->
691
+ <div id="tgStep3" style="display:none">
692
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
693
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">3</span>
694
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Test &amp; Enable</span>
695
+ </div>
696
+ <div style="margin-left:30px;margin-bottom:12px;padding:10px;background:var(--input-bg);border-radius:6px;font-size:12px;color:var(--text-dim)">
697
+ <div>Bot Token: <span id="tgSummaryToken" style="color:var(--text);font-family:monospace"></span></div>
698
+ <div style="margin-top:4px">Chat ID: <span id="tgSummaryChatId" style="color:var(--text);font-family:monospace"></span></div>
699
+ </div>
700
+ <div style="margin-left:30px;margin-bottom:12px;display:flex;align-items:center;gap:8px">
701
+ <input type="checkbox" id="tgEnabled" checked style="width:16px;height:16px;accent-color:var(--accent)">
702
+ <label for="tgEnabled" style="font-size:13px;color:var(--text)">Enable Telegram notifications</label>
703
+ </div>
704
+ <div id="tgStatus" style="display:none;margin:0 0 12px 30px;padding:8px 12px;border-radius:6px;font-size:12px"></div>
705
+ <div style="margin-left:30px;display:flex;gap:8px">
706
+ <button id="tgBackStep2" style="padding:8px 20px;font-size:13px;background:none;color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer">← Back</button>
707
+ <button id="tgTest" style="flex:1;padding:10px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-size:13px;font-weight:500">Send Test</button>
708
+ <button id="tgSave" style="flex:1;padding:10px">Save</button>
709
+ <button id="settingsCancel2" style="padding:10px 16px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:13px">Cancel</button>
710
+ </div>
671
711
  </div>
672
712
  </div>
673
713
  </div>
@@ -1051,20 +1091,19 @@
1051
1091
  if (!content && !hasImage) return;
1052
1092
  if (!state.selectedSession || !state.ws) return;
1053
1093
 
1054
- // Send text
1055
- if (content) {
1056
- state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content }));
1057
- }
1058
-
1059
- // Send image
1060
1094
  if (hasImage) {
1095
+ // Send image + text together
1061
1096
  state.ws.send(JSON.stringify({
1062
1097
  type: 'image_upload',
1063
1098
  sessionId: state.selectedSession,
1064
1099
  imageData: state.pendingImage.data,
1065
1100
  mimeType: state.pendingImage.mimeType,
1066
1101
  originalName: state.pendingImage.name,
1102
+ content: content || '',
1067
1103
  }));
1104
+ } else if (content) {
1105
+ // Text only
1106
+ state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content }));
1068
1107
  }
1069
1108
 
1070
1109
  // Add to local messages
@@ -1218,7 +1257,17 @@
1218
1257
  }
1219
1258
  $('#tgChatId').value = tg.chatId || '';
1220
1259
  $('#tgEnabled').checked = !!tg.enabled;
1221
- } catch {}
1260
+ // Jump to appropriate step
1261
+ if (tg.chatId && tg.botToken) {
1262
+ $('#tgSummaryToken').textContent = (tg.botToken || '').slice(0, 8) + '...';
1263
+ $('#tgSummaryChatId').textContent = tg.chatId;
1264
+ showTgStep(3);
1265
+ } else if (tg.botToken) {
1266
+ showTgStep(2);
1267
+ } else {
1268
+ showTgStep(1);
1269
+ }
1270
+ } catch { showTgStep(1); }
1222
1271
  $('#tgStatus').style.display = 'none';
1223
1272
  $('#settingsOverlay').classList.remove('hidden');
1224
1273
  });
@@ -1269,7 +1318,13 @@
1269
1318
  });
1270
1319
  }
1271
1320
 
1272
- // Telegram
1321
+ // Telegram wizard
1322
+ function showTgStep(step) {
1323
+ $('#tgStep1').style.display = step === 1 ? 'block' : 'none';
1324
+ $('#tgStep2').style.display = step === 2 ? 'block' : 'none';
1325
+ $('#tgStep3').style.display = step === 3 ? 'block' : 'none';
1326
+ }
1327
+
1273
1328
  function showTgStatus(msg, ok) {
1274
1329
  const el = $('#tgStatus');
1275
1330
  el.textContent = msg;
@@ -1278,11 +1333,70 @@
1278
1333
  el.style.color = ok ? 'var(--green)' : 'var(--red)';
1279
1334
  }
1280
1335
 
1336
+ // Step navigation
1337
+ $('#tgNextStep2').addEventListener('click', () => {
1338
+ const token = $('#tgBotToken').value.trim();
1339
+ if (!token) { $('#tgBotToken').style.borderColor = 'var(--red)'; return; }
1340
+ $('#tgBotToken').style.borderColor = 'var(--border)';
1341
+ showTgStep(2);
1342
+ });
1343
+ $('#tgBackStep1').addEventListener('click', () => showTgStep(1));
1344
+ $('#tgNextStep3').addEventListener('click', () => {
1345
+ const chatId = $('#tgChatId').value.trim();
1346
+ if (!chatId) { $('#tgChatId').style.borderColor = 'var(--red)'; return; }
1347
+ $('#tgChatId').style.borderColor = 'var(--border)';
1348
+ const token = $('#tgBotToken').value.trim();
1349
+ $('#tgSummaryToken').textContent = token.slice(0, 8) + '...';
1350
+ $('#tgSummaryChatId').textContent = chatId;
1351
+ $('#tgStatus').style.display = 'none';
1352
+ showTgStep(3);
1353
+ });
1354
+ $('#tgBackStep2').addEventListener('click', () => showTgStep(2));
1355
+
1356
+ // Detect Chat ID
1357
+ $('#tgDetect').addEventListener('click', async () => {
1358
+ const botToken = $('#tgBotToken').value.trim();
1359
+ if (!botToken || botToken.includes('...')) { $('#tgDetectStatus').textContent = 'Enter a valid Bot Token first.'; return; }
1360
+ $('#tgDetectStatus').textContent = 'Detecting...';
1361
+ $('#tgChatList').innerHTML = '';
1362
+ try {
1363
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1364
+ const res = await fetch(`/api/telegram/detect${tokenQuery}`, {
1365
+ method: 'POST',
1366
+ headers: { 'Content-Type': 'application/json' },
1367
+ body: JSON.stringify({ botToken }),
1368
+ });
1369
+ const data = await res.json();
1370
+ if (data.error) { $('#tgDetectStatus').textContent = data.error; return; }
1371
+ if (!data.ok || !data.chats.length) {
1372
+ $('#tgDetectStatus').textContent = 'No messages found. Send a message to your bot first!';
1373
+ return;
1374
+ }
1375
+ $('#tgDetectStatus').textContent = 'Found ' + data.chats.length + ' chat(s):';
1376
+ $('#tgChatList').innerHTML = data.chats.map(c =>
1377
+ `<button class="tg-chat-pick" data-id="${esc(c.id)}" style="display:block;width:100%;text-align:left;padding:8px 12px;margin-bottom:4px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);cursor:pointer;font-size:12px;transition:border-color 0.15s">
1378
+ <strong>${esc(c.name)}</strong> <span style="color:var(--text-dim)">(${esc(c.type)}, ID: ${esc(c.id)})</span>
1379
+ </button>`
1380
+ ).join('');
1381
+ $('#tgChatList').querySelectorAll('.tg-chat-pick').forEach(btn => {
1382
+ btn.addEventListener('click', () => {
1383
+ $('#tgChatId').value = btn.dataset.id;
1384
+ $('#tgChatList').querySelectorAll('.tg-chat-pick').forEach(b => b.style.borderColor = 'var(--border)');
1385
+ btn.style.borderColor = 'var(--accent)';
1386
+ });
1387
+ btn.addEventListener('mouseenter', () => { btn.style.borderColor = 'var(--accent)'; });
1388
+ btn.addEventListener('mouseleave', () => {
1389
+ if ($('#tgChatId').value !== btn.dataset.id) btn.style.borderColor = 'var(--border)';
1390
+ });
1391
+ });
1392
+ } catch { $('#tgDetectStatus').textContent = 'Connection error.'; }
1393
+ });
1394
+
1395
+ // Test
1281
1396
  $('#tgTest').addEventListener('click', async () => {
1282
1397
  const botToken = $('#tgBotToken').value.trim();
1283
1398
  const chatId = $('#tgChatId').value.trim();
1284
1399
  if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
1285
- if (botToken.includes('...')) { showTgStatus('Please enter full Bot Token (not masked).', false); return; }
1286
1400
  try {
1287
1401
  const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1288
1402
  const res = await fetch(`/api/telegram/test${tokenQuery}`, {
@@ -1296,11 +1410,12 @@
1296
1410
  } catch (e) { showTgStatus('Connection error.', false); }
1297
1411
  });
1298
1412
 
1413
+ // Save
1299
1414
  $('#tgSave').addEventListener('click', async () => {
1300
1415
  const botToken = $('#tgBotToken').value.trim();
1301
1416
  const chatId = $('#tgChatId').value.trim();
1302
1417
  const enabled = $('#tgEnabled').checked;
1303
- if (enabled && botToken.includes('...')) { showTgStatus('Please enter full Bot Token to enable.', false); return; }
1418
+ if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
1304
1419
  try {
1305
1420
  const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1306
1421
  await fetch(`/api/telegram${tokenQuery}`, {
@@ -551,6 +551,8 @@ var HubServer = class {
551
551
  this.handleTelegramSave(req, res);
552
552
  } else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
553
553
  this.handleTelegramTest(req, res);
554
+ } else if (url.pathname === "/api/telegram/detect" && req.method === "POST") {
555
+ this.handleTelegramDetect(req, res);
554
556
  } else {
555
557
  this.jsonResponse(res, 404, { error: "Not found" });
556
558
  }
@@ -736,7 +738,7 @@ var HubServer = class {
736
738
  }
737
739
  }
738
740
  handleImageUpload(msg) {
739
- const { sessionId, imageData, mimeType, originalName } = msg;
741
+ const { sessionId, imageData, mimeType, originalName, content } = msg;
740
742
  if (!this.localChannels.has(sessionId)) {
741
743
  logger.warn(`Image upload rejected: session ${sessionId} is not local`);
742
744
  return;
@@ -761,7 +763,8 @@ var HubServer = class {
761
763
  sessionId,
762
764
  imagePath: filePath,
763
765
  mimeType,
764
- originalName
766
+ originalName,
767
+ content
765
768
  };
766
769
  channelWs.send(JSON.stringify(forwardMsg));
767
770
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
@@ -858,6 +861,50 @@ var HubServer = class {
858
861
  this.jsonResponse(res, 500, { error: err.message });
859
862
  }
860
863
  }
864
+ async handleTelegramDetect(req, res) {
865
+ const body = await this.readBody(req);
866
+ if (!body) {
867
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
868
+ return;
869
+ }
870
+ const { botToken } = body;
871
+ if (!botToken) {
872
+ this.jsonResponse(res, 400, { error: "botToken required" });
873
+ return;
874
+ }
875
+ try {
876
+ const detectRes = await fetch(`https://api.telegram.org/bot${botToken}/getUpdates?timeout=0&limit=10`, {
877
+ signal: AbortSignal.timeout(1e4)
878
+ });
879
+ if (!detectRes.ok) {
880
+ const err = await detectRes.json();
881
+ this.jsonResponse(res, 400, { error: err.description || "Invalid bot token" });
882
+ return;
883
+ }
884
+ const data = await detectRes.json();
885
+ if (!data.ok || !data.result.length) {
886
+ this.jsonResponse(res, 200, { ok: false, chats: [] });
887
+ return;
888
+ }
889
+ const chatMap = /* @__PURE__ */ new Map();
890
+ for (const update of data.result) {
891
+ if (update.message?.chat) {
892
+ const chat = update.message.chat;
893
+ const id = String(chat.id);
894
+ if (!chatMap.has(id)) {
895
+ chatMap.set(id, {
896
+ id,
897
+ name: chat.title || chat.first_name || id,
898
+ type: chat.type
899
+ });
900
+ }
901
+ }
902
+ }
903
+ this.jsonResponse(res, 200, { ok: true, chats: [...chatMap.values()] });
904
+ } catch (err) {
905
+ this.jsonResponse(res, 500, { error: err.message });
906
+ }
907
+ }
861
908
  cleanupUploads() {
862
909
  try {
863
910
  if (!fs2.existsSync(UPLOADS_DIR)) return;