@hienlh/ppm 0.9.39 → 0.9.40

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/web/assets/{browser-tab-DnIsHiCc.js → browser-tab-Dm65lWLO.js} +1 -1
  3. package/dist/web/assets/{chat-tab-il6D4jql.js → chat-tab-rKXwCBEZ.js} +1 -1
  4. package/dist/web/assets/{code-editor-BUc1jBqm.js → code-editor-BD_hxR8Z.js} +1 -1
  5. package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-RqbZkczM.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-C-6EcVDG.js} +1 -1
  7. package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-xZQrFpb0.js} +1 -1
  8. package/dist/web/assets/{git-graph-4eGJ8B1A.js → git-graph-Cndi59vr.js} +1 -1
  9. package/dist/web/assets/index-BpOBp5oT.js +30 -0
  10. package/dist/web/assets/keybindings-store-BE5y0cut.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-CViGEOCg.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-DjRZKruo.js} +1 -1
  13. package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DQ3eb1bU.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-B8OuhoEV.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-D7K74k2B.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-BEWkUA66.js} +1 -1
  17. package/dist/web/index.html +1 -1
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/server/routes/settings.ts +11 -16
  21. package/src/services/db.service.ts +6 -0
  22. package/src/services/telegram-notification.service.ts +44 -21
  23. package/src/types/config.ts +0 -2
  24. package/src/web/components/settings/ppmbot-settings-section.tsx +87 -2
  25. package/src/web/components/settings/settings-tab.tsx +6 -4
  26. package/dist/web/assets/index-BmcV1di6.js +0 -30
  27. package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { configService } from "./config.service.ts";
2
2
  import { tunnelService } from "./tunnel.service.ts";
3
3
  import { getLocalIp } from "../lib/network-utils.ts";
4
+ import { getApprovedPairedChats } from "./db.service.ts";
4
5
  import type { TelegramConfig } from "../types/config.ts";
5
6
  import type { NotificationPayload } from "./notification.service.ts";
6
7
 
@@ -12,12 +13,15 @@ function escapeHtml(str: string): string {
12
13
  }
13
14
 
14
15
  class TelegramNotificationService {
15
- /** Send notification to Telegram. No-op if not configured. */
16
+ /** Send notification to all approved paired chats. No-op if not configured. */
16
17
  async send(payload: NotificationPayload): Promise<void> {
17
18
  const config = configService.get("telegram") as TelegramConfig | undefined;
18
- if (!config?.bot_token || !config?.chat_id) return;
19
+ if (!config?.bot_token) return;
19
20
  if (!BOT_TOKEN_RE.test(config.bot_token)) return;
20
21
 
22
+ const approvedChats = getApprovedPairedChats();
23
+ if (approvedChats.length === 0) return;
24
+
21
25
  const deviceName = (configService.get("device_name") as string) || "PPM";
22
26
  const deepLink = this.buildDeepLink(payload);
23
27
 
@@ -27,31 +31,50 @@ class TelegramNotificationService {
27
31
  text += `\n\n<a href="${deepLink}">Open in PPM</a>`;
28
32
  }
29
33
 
30
- await this.callApi(config.bot_token, config.chat_id, text);
34
+ // Send to all approved paired chats in parallel
35
+ await Promise.allSettled(
36
+ approvedChats.map((chat) =>
37
+ this.callApi(config.bot_token, chat.telegram_chat_id, text),
38
+ ),
39
+ );
31
40
  }
32
41
 
33
- /** Send a test message. Returns { ok, error? } */
34
- async sendTest(botToken: string, chatId: string): Promise<{ ok: boolean; error?: string }> {
42
+ /** Send a test message to all approved paired chats. Returns { ok, error? } */
43
+ async sendTest(botToken: string): Promise<{ ok: boolean; error?: string }> {
35
44
  if (!BOT_TOKEN_RE.test(botToken)) return { ok: false, error: "Invalid bot token format" };
45
+
46
+ const approvedChats = getApprovedPairedChats();
47
+ if (approvedChats.length === 0) {
48
+ return { ok: false, error: "No approved paired chats. Pair a device in PPMBot settings first." };
49
+ }
50
+
36
51
  const deviceName = (configService.get("device_name") as string) || "PPM";
37
52
  const text = `<b>${escapeHtml(deviceName)} — Test</b>\nTelegram notifications are working!`;
38
- const controller = new AbortController();
39
- const timeout = setTimeout(() => controller.abort(), 10_000);
40
- try {
41
- const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
42
- method: "POST",
43
- headers: { "Content-Type": "application/json" },
44
- body: JSON.stringify({ chat_id: chatId, text, parse_mode: "HTML" }),
45
- signal: controller.signal,
46
- });
47
- const json = (await res.json()) as { ok: boolean; description?: string };
48
- if (!json.ok) return { ok: false, error: json.description || "Unknown error" };
49
- return { ok: true };
50
- } catch (e) {
51
- return { ok: false, error: (e as Error).message };
52
- } finally {
53
- clearTimeout(timeout);
53
+
54
+ const results = await Promise.allSettled(
55
+ approvedChats.map(async (chat) => {
56
+ const controller = new AbortController();
57
+ const timeout = setTimeout(() => controller.abort(), 10_000);
58
+ try {
59
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ chat_id: chat.telegram_chat_id, text, parse_mode: "HTML" }),
63
+ signal: controller.signal,
64
+ });
65
+ const json = (await res.json()) as { ok: boolean; description?: string };
66
+ if (!json.ok) throw new Error(json.description || "Unknown error");
67
+ } finally {
68
+ clearTimeout(timeout);
69
+ }
70
+ }),
71
+ );
72
+
73
+ const failed = results.filter((r) => r.status === "rejected");
74
+ if (failed.length === results.length) {
75
+ return { ok: false, error: (failed[0] as PromiseRejectedResult).reason?.message || "All sends failed" };
54
76
  }
77
+ return { ok: true };
55
78
  }
56
79
 
57
80
  private buildDeepLink(payload: NotificationPayload): string | null {
@@ -6,7 +6,6 @@ export interface PushConfig {
6
6
 
7
7
  export interface TelegramConfig {
8
8
  bot_token: string;
9
- chat_id: string;
10
9
  }
11
10
 
12
11
  export interface PPMBotConfig {
@@ -99,7 +98,6 @@ export const DEFAULT_CONFIG: PpmConfig = {
99
98
  },
100
99
  telegram: {
101
100
  bot_token: "",
102
- chat_id: "",
103
101
  },
104
102
  clawbot: {
105
103
  enabled: false,
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Switch } from "@/components/ui/switch";
5
5
  import { api } from "@/lib/api-client";
6
- import { Trash2, CheckCircle, Clock } from "lucide-react";
6
+ import { Trash2, CheckCircle, Clock, Send } from "lucide-react";
7
7
 
8
8
  interface PPMBotConfig {
9
9
  enabled: boolean;
@@ -16,6 +16,10 @@ interface PPMBotConfig {
16
16
  debounce_ms: number;
17
17
  }
18
18
 
19
+ interface TelegramConfig {
20
+ bot_token: string;
21
+ }
22
+
19
23
  interface PairedChat {
20
24
  id: number;
21
25
  telegram_chat_id: string;
@@ -32,6 +36,11 @@ export function PPMBotSettingsSection() {
32
36
  const [saving, setSaving] = useState(false);
33
37
  const [status, setStatus] = useState<{ type: "ok" | "err"; msg: string } | null>(null);
34
38
 
39
+ // Bot token (from telegram config)
40
+ const [tokenInput, setTokenInput] = useState("");
41
+ const [tokenConfigured, setTokenConfigured] = useState(false);
42
+ const [tokenSaving, setTokenSaving] = useState(false);
43
+
35
44
  const [enabled, setEnabled] = useState(false);
36
45
  const [defaultProject, setDefaultProject] = useState("");
37
46
  const [systemPrompt, setSystemPrompt] = useState("");
@@ -42,6 +51,7 @@ export function PPMBotSettingsSection() {
42
51
  const [pairedChats, setPairedChats] = useState<PairedChat[]>([]);
43
52
  const [approveCode, setApproveCode] = useState("");
44
53
  const [approving, setApproving] = useState(false);
54
+ const [testing, setTesting] = useState(false);
45
55
 
46
56
  const fetchPairedChats = useCallback(async () => {
47
57
  try {
@@ -60,9 +70,28 @@ export function PPMBotSettingsSection() {
60
70
  setShowThinking(data.show_thinking);
61
71
  setDebounceMs(data.debounce_ms);
62
72
  }).catch(() => {});
73
+ api.get<TelegramConfig>("/api/settings/telegram").then((data) => {
74
+ setTokenConfigured(!!data.bot_token);
75
+ }).catch(() => {});
63
76
  fetchPairedChats();
64
77
  }, [fetchPairedChats]);
65
78
 
79
+ const saveToken = async () => {
80
+ if (!tokenInput.trim()) return;
81
+ setTokenSaving(true);
82
+ setStatus(null);
83
+ try {
84
+ await api.put<TelegramConfig>("/api/settings/telegram", { bot_token: tokenInput });
85
+ setTokenConfigured(true);
86
+ setTokenInput("");
87
+ setStatus({ type: "ok", msg: "Bot token saved" });
88
+ } catch (e) {
89
+ setStatus({ type: "err", msg: (e as Error).message });
90
+ } finally {
91
+ setTokenSaving(false);
92
+ }
93
+ };
94
+
66
95
  const save = async () => {
67
96
  setSaving(true);
68
97
  setStatus(null);
@@ -110,10 +139,51 @@ export function PPMBotSettingsSection() {
110
139
  }
111
140
  };
112
141
 
142
+ const handleTestNotification = async () => {
143
+ setTesting(true);
144
+ setStatus(null);
145
+ try {
146
+ await api.post("/api/settings/telegram/test", {});
147
+ setStatus({ type: "ok", msg: "Test notification sent to all paired devices!" });
148
+ } catch (e) {
149
+ setStatus({ type: "err", msg: (e as Error).message });
150
+ } finally {
151
+ setTesting(false);
152
+ }
153
+ };
154
+
113
155
  if (!config) return <p className="text-xs text-muted-foreground">Loading...</p>;
114
156
 
157
+ const approvedCount = pairedChats.filter((c) => c.status === "approved").length;
158
+
115
159
  return (
116
160
  <div className="space-y-4">
161
+ {/* Bot Token */}
162
+ <div className="space-y-1.5">
163
+ <label className="text-[11px] text-muted-foreground">Telegram Bot Token</label>
164
+ <div className="flex gap-1.5">
165
+ <Input
166
+ type="password"
167
+ placeholder={tokenConfigured ? "•••••• (saved)" : "123456:ABC-DEF..."}
168
+ value={tokenInput}
169
+ onChange={(e) => setTokenInput(e.target.value)}
170
+ className="h-7 text-xs flex-1"
171
+ />
172
+ <Button
173
+ variant="outline"
174
+ size="sm"
175
+ className="h-7 text-xs shrink-0 cursor-pointer"
176
+ disabled={tokenSaving || !tokenInput.trim()}
177
+ onClick={saveToken}
178
+ >
179
+ {tokenSaving ? "..." : "Save"}
180
+ </Button>
181
+ </div>
182
+ <p className="text-[10px] text-muted-foreground">
183
+ Create a bot via <b>@BotFather</b> on Telegram. Used for both chat and notifications.
184
+ </p>
185
+ </div>
186
+
117
187
  {/* Enable/Disable */}
118
188
  <div className="flex items-center justify-between">
119
189
  <div>
@@ -130,6 +200,7 @@ export function PPMBotSettingsSection() {
130
200
  <p className="text-xs font-medium">Paired Devices</p>
131
201
  <p className="text-[10px] text-muted-foreground">
132
202
  Send any message to the bot on Telegram to get a pairing code. Enter it below to approve.
203
+ Notifications are sent to all approved devices.
133
204
  </p>
134
205
 
135
206
  <div className="flex gap-2">
@@ -189,6 +260,20 @@ export function PPMBotSettingsSection() {
189
260
  ))}
190
261
  </div>
191
262
  )}
263
+
264
+ {/* Test notification button */}
265
+ {tokenConfigured && approvedCount > 0 && (
266
+ <Button
267
+ variant="outline"
268
+ size="sm"
269
+ className="h-7 text-xs gap-1 w-full cursor-pointer"
270
+ disabled={testing}
271
+ onClick={handleTestNotification}
272
+ >
273
+ <Send className="size-3" />
274
+ {testing ? "Sending..." : "Test Notification"}
275
+ </Button>
276
+ )}
192
277
  </div>
193
278
 
194
279
  {/* Default Project */}
@@ -201,7 +286,7 @@ export function PPMBotSettingsSection() {
201
286
  className="h-7 text-xs"
202
287
  />
203
288
  <p className="text-[10px] text-muted-foreground">
204
- Project used when starting a new chat. Must match a project name in PPM.
289
+ Project used when starting a new chat. Leave empty for default workspace (~/.ppm/bot/).
205
290
  </p>
206
291
  </div>
207
292
 
@@ -11,7 +11,6 @@ import { useSettingsStore, type Theme } from "@/stores/settings-store";
11
11
  import { cn } from "@/lib/utils";
12
12
  import { AISettingsSection } from "./ai-settings-section";
13
13
  import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
14
- import { TelegramSettingsSection } from "./telegram-settings-section";
15
14
  import { ProxySettingsSection } from "./proxy-settings-section";
16
15
  import { McpSettingsSection } from "./mcp-settings-section";
17
16
  import { ExtensionManagerSection } from "./extension-manager-section";
@@ -276,10 +275,13 @@ function NotificationsContent({ isSubscribed, loading, permission, pushError, su
276
275
 
277
276
  <Separator />
278
277
 
279
- {/* Telegram */}
280
- <section className="space-y-2">
278
+ {/* Telegram — redirect to PPMBot */}
279
+ <section className="space-y-1">
281
280
  <h3 className="text-xs font-medium text-muted-foreground">Telegram</h3>
282
- <TelegramSettingsSection />
281
+ <p className="text-[11px] text-muted-foreground">
282
+ Telegram notifications are sent to all approved devices in PPMBot settings.
283
+ Configure your bot token and pair devices there.
284
+ </p>
283
285
  </section>
284
286
  </div>
285
287
  );