feat(telegram): notify on Gitea issue creation

apps/web/src/lib/telegram.ts: server-side Telegram client + alertIssueCreated.
createGithubIssueForInsight action fires the alert after issue is created (after
audit log, before revalidate). Severity emoji + insight link + issue link.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-05-14 12:44:13 +00:00
parent b38721bb05
commit f93cabb932
3 changed files with 76 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import { auth } from "@/lib/auth";
import { writeAudit } from "@/lib/audit";
import { createIssue, buildIssueBody } from "@/lib/gitea";
import { pipelineQueue } from "@/lib/queue";
import { alertIssueCreated } from "@/lib/telegram";
const ALLOWED_STATUS = new Set([
"new",
@@ -161,9 +162,21 @@ export async function createGithubIssueForInsight(
await writeAudit({
endpoint: `/insights/${insightId}/github-issue`,
method: "POST",
requestPayload: { issueNumber: issue.number, repo: process.env[`GITHUB_REPO_${insight.projectKey.toUpperCase()}`] },
requestPayload: { issueNumber: issue.number, repo: process.env[`GITEA_REPO_${insight.projectKey.toUpperCase()}`] },
responseStatus: 200,
});
// Telegram notification (fire-and-forget)
void alertIssueCreated({
insightId: insight.id,
insightTitle: insight.title,
severity: insight.severity,
type: insight.type,
issueNumber: issue.number,
issueUrl: issue.html_url,
panelUrl,
});
revalidatePath("/insights");
revalidatePath(`/insights/i/${insightId}`);
return { url: issue.html_url, number: issue.number };

View File

@@ -0,0 +1,61 @@
// Server-side Telegram client for the web app (issue creation, manual triggers, etc).
// Fire-and-forget; failures swallowed.
const TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? "";
const CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? "";
export function isTelegramConfigured(): boolean {
return Boolean(TOKEN && CHAT_ID);
}
export async function sendTelegram(opts: {
text: string;
silent?: boolean;
parseMode?: "HTML" | "Markdown";
}): Promise<{ ok: boolean; error?: string }> {
if (!isTelegramConfigured()) return { ok: false, error: "not_configured" };
try {
const res = await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text: opts.text,
parse_mode: opts.parseMode ?? "HTML",
disable_notification: opts.silent ?? false,
disable_web_page_preview: true,
}),
});
if (!res.ok) {
const err = await res.text();
return { ok: false, error: `${res.status}: ${err.slice(0, 200)}` };
}
return { ok: true };
} catch (e) {
return { ok: false, error: (e as Error).message };
}
}
export function alertIssueCreated(opts: {
insightId: string;
insightTitle: string;
severity: string;
type: string;
issueNumber: number;
issueUrl: string;
panelUrl: string;
}): Promise<{ ok: boolean; error?: string }> {
const sev = opts.severity === "P0" || opts.severity === "P1" ? "🔴" : opts.severity === "P2" ? "🟡" : "⚪";
const text = [
`🎟️ <b>Gitea issue opened</b>`,
`${sev} <b>${opts.severity}</b> · ${escapeHtml(opts.type)}`,
`<b>${escapeHtml(opts.insightTitle)}</b>`,
``,
`<a href="${opts.issueUrl}">#${opts.issueNumber} on Gitea</a> · <a href="${opts.panelUrl}/insights/i/${opts.insightId}">Insight in panel</a>`,
].join("\n");
return sendTelegram({ text });
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

File diff suppressed because one or more lines are too long