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 && (
+
+ )}
+
+ {webhookStatus.message && (
+
+ {webhookStatus.message}
+
+ )}
+
+
+
{/* App Info */}
{APP_CONFIG.name} v{APP_CONFIG.version}
diff --git a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js
index a80f19e39..da2f2a864 100644
--- a/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js
+++ b/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js
@@ -3,6 +3,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import PropTypes from "prop-types";
import { Card, Badge, Button, Modal, Select, Toggle, EditConnectionModal } from "@/shared/components";
+import ErrorHistoryModal from "./ErrorHistoryModal";
// ── CooldownTimer ──────────────────────────────────────────────
function CooldownTimer({ until }) {
@@ -29,7 +30,7 @@ function CooldownTimer({ until }) {
CooldownTimer.propTypes = { until: PropTypes.string.isRequired };
// ── ConnectionRow ──────────────────────────────────────────────
-function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete }) {
+function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete, onShowErrorHistory }) {
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
const [updatingProxy, setUpdatingProxy] = useState(false);
const [isCooldown, setIsCooldown] = useState(false);
@@ -159,6 +160,10 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
)}
)}
+