import { db } from "@/lib/db"; function allowed(bucket: string, seconds = 3600) { const now = Math.floor(Date.now() / 1000); const row = db.prepare("SELECT reset_at FROM rate_limits WHERE bucket=?").get(bucket) as { reset_at: number } | undefined; if (row && row.reset_at > now) return false; db.prepare("INSERT INTO rate_limits(bucket,count,reset_at) VALUES(?,1,?) ON CONFLICT(bucket) DO UPDATE SET count=count+1,reset_at=excluded.reset_at").run(bucket, now + seconds); return true; } export function sendAlert(bucket: string, title: string, message: string) { if (!process.env.ALERT_WEBHOOK_URL?.trim() || !allowed(`alert:${bucket}`)) return false; db.prepare("INSERT INTO alert_deliveries(bucket,title,message) VALUES(?,?,?)").run(bucket, title, message); return true; } type Alert = { id: number; title: string; message: string; attempts: number }; export async function deliverNextAlert() { const candidate = db.prepare("SELECT id FROM alert_deliveries WHERE status='queued' AND run_after<=CURRENT_TIMESTAMP ORDER BY id LIMIT 1").get() as { id: number } | undefined; if (!candidate) return false; const claimed = db.prepare("UPDATE alert_deliveries SET status='running',attempts=attempts+1,started_at=CURRENT_TIMESTAMP WHERE id=? AND status='queued'").run(candidate.id); if (!claimed.changes) return false; const alert = db.prepare("SELECT id,title,message,attempts FROM alert_deliveries WHERE id=?").get(candidate.id) as Alert; const url = process.env.ALERT_WEBHOOK_URL?.trim(); try { if (!url) throw new Error("ALERT_WEBHOOK_URL is not configured"); const target = new URL(url); if (target.protocol !== "https:") throw new Error("Alert webhook must use HTTPS"); const isNtfy = /(^|\.)ntfy\.sh$/.test(target.hostname); const response = await fetch(target, isNtfy ? { method: "POST", headers: { Title: alert.title, Priority: "high" }, body: alert.message, signal: AbortSignal.timeout(10_000) } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: `**${alert.title}**\n${alert.message}` }), signal: AbortSignal.timeout(10_000) }); if (!response.ok) throw new Error(`Alert endpoint returned ${response.status}`); db.prepare("UPDATE alert_deliveries SET status='done',finished_at=CURRENT_TIMESTAMP,last_error=NULL WHERE id=?").run(alert.id); } catch (error) { const message = error instanceof Error ? error.message : "Alert delivery failed"; const exhausted = alert.attempts >= 5; const delayMinutes = Math.min(60, 2 ** Math.max(0, alert.attempts - 1)); db.prepare("UPDATE alert_deliveries SET status=?,last_error=?,finished_at=CASE WHEN ? THEN CURRENT_TIMESTAMP ELSE NULL END,run_after=CASE WHEN ? THEN run_after ELSE datetime('now', ?) END WHERE id=?").run(exhausted ? "failed" : "queued", message, exhausted ? 1 : 0, exhausted ? 1 : 0, `+${delayMinutes} minutes`, alert.id); } return true; }