diff --git a/package.json b/package.json index e05e262db..d92558254 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@monaco-editor/react": "^4.7.0", + "@toanalien/ahha": "^0.1.1", "@xyflow/react": "^12.10.1", "bcryptjs": "^3.0.3", "confbox": "^0.2.4", diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 27310276c..d8332c75d 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -24,6 +24,15 @@ export default function ProfilePage() { const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" }); const [proxyLoading, setProxyLoading] = useState(false); const [proxyTestLoading, setProxyTestLoading] = useState(false); + const [webhookForm, setWebhookForm] = useState({ + webhookEnabled: false, + webhookUrls: "", + webhookThrottleMs: 5, + webhookErrorCodes: [401, 403, 429, 500, 502, 503], + }); + const [webhookStatus, setWebhookStatus] = useState({ type: "", message: "" }); + const [webhookLoading, setWebhookLoading] = useState(false); + const [webhookTestLoading, setWebhookTestLoading] = useState(false); useEffect(() => { fetch("/api/settings") @@ -35,6 +44,12 @@ export default function ProfilePage() { outboundProxyUrl: data?.outboundProxyUrl || "", outboundNoProxy: data?.outboundNoProxy || "", }); + setWebhookForm({ + webhookEnabled: data?.webhookEnabled === true, + webhookUrls: (data?.webhookUrls || []).join("\n"), + webhookThrottleMs: Math.round((data?.webhookThrottleMs || 300000) / 60000), + webhookErrorCodes: data?.webhookErrorCodes || [401, 403, 429, 500, 502, 503], + }); setLoading(false); }) .catch((err) => { @@ -140,6 +155,96 @@ export default function ProfilePage() { } }; + const updateWebhookEnabled = async (enabled) => { + setWebhookLoading(true); + setWebhookStatus({ type: "", message: "" }); + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ webhookEnabled: enabled }), + }); + const data = await res.json(); + if (res.ok) { + setSettings((prev) => ({ ...prev, ...data })); + setWebhookForm((prev) => ({ ...prev, webhookEnabled: enabled })); + setWebhookStatus({ type: "success", message: enabled ? "Webhook enabled" : "Webhook disabled" }); + } else { + setWebhookStatus({ type: "error", message: data.error || "Failed to update" }); + } + } catch { + setWebhookStatus({ type: "error", message: "An error occurred" }); + } finally { + setWebhookLoading(false); + } + }; + + const saveWebhookSettings = async (e) => { + e.preventDefault(); + setWebhookLoading(true); + setWebhookStatus({ type: "", message: "" }); + try { + const urls = webhookForm.webhookUrls.split("\n").map(u => u.trim()).filter(Boolean); + const throttleMs = Math.max(1, webhookForm.webhookThrottleMs) * 60000; + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + webhookUrls: urls, + webhookThrottleMs: throttleMs, + webhookErrorCodes: webhookForm.webhookErrorCodes, + }), + }); + const data = await res.json(); + if (res.ok) { + setSettings((prev) => ({ ...prev, ...data })); + setWebhookStatus({ type: "success", message: "Webhook settings saved" }); + } else { + setWebhookStatus({ type: "error", message: data.error || "Failed to save" }); + } + } catch { + setWebhookStatus({ type: "error", message: "An error occurred" }); + } finally { + setWebhookLoading(false); + } + }; + + const testWebhook = async () => { + const urls = webhookForm.webhookUrls.split("\n").map(u => u.trim()).filter(Boolean); + if (urls.length === 0) { + setWebhookStatus({ type: "error", message: "Please enter at least one webhook URL" }); + return; + } + setWebhookTestLoading(true); + setWebhookStatus({ type: "", message: "" }); + try { + const res = await fetch("/api/settings/webhook-test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ urls }), + }); + const data = await res.json(); + if (data.success) { + setWebhookStatus({ type: "success", message: "Test notification sent successfully" }); + } else { + setWebhookStatus({ type: "error", message: data.error || "Test failed" }); + } + } catch { + setWebhookStatus({ type: "error", message: "An error occurred" }); + } finally { + setWebhookTestLoading(false); + } + }; + + const toggleErrorCode = (code) => { + setWebhookForm((prev) => { + const codes = prev.webhookErrorCodes.includes(code) + ? prev.webhookErrorCodes.filter(c => c !== code) + : [...prev.webhookErrorCodes, code]; + return { ...prev, webhookErrorCodes: codes }; + }); + }; + const handlePasswordChange = async (e) => { e.preventDefault(); if (passwords.new !== passwords.confirm) { @@ -650,6 +755,108 @@ export default function ProfilePage() { + {/* Webhook Notifications */} + +
+
+ notifications +
+

Notifications

+
+ +
+
+
+

Webhook Notifications

+

+ Send alerts via webhook when provider errors occur (401, 429, etc.) +

+
+ updateWebhookEnabled(!webhookForm.webhookEnabled)} + disabled={loading || webhookLoading} + /> +
+ + {webhookForm.webhookEnabled && ( +
+
+ +