From 3dd27874b1ecae41181a33b8668c3139d4c96f57 Mon Sep 17 00:00:00 2001 From: rangarajan19 Date: Sat, 10 Oct 2026 20:47:44 +0530 Subject: [PATCH] feat(settings): add the Connector Failure Alerts card to admin settings UI for the alert-webhook backend from #1006 (PR 1, merged): URL, payload type (JSON/Slack), threshold/window/cooldown, enabled toggle, save/test/ remove, and a rotate-secret option with a shown-once secret reveal, mirroring the existing SMTP card's conventions on the same page. --- .../frontend/src/app/settings/admin/page.tsx | 233 +++++++++++++++++- packages/frontend/src/lib/api.ts | 30 +++ 2 files changed, 261 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/app/settings/admin/page.tsx b/packages/frontend/src/app/settings/admin/page.tsx index 9c0f6b79..a91c6ee7 100644 --- a/packages/frontend/src/app/settings/admin/page.tsx +++ b/packages/frontend/src/app/settings/admin/page.tsx @@ -3,12 +3,20 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useAuth } from '@/lib/auth-context'; -import { adminSettings } from '@/lib/api'; +import { adminSettings, type AlertWebhookInput } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; +import { AppSelect } from '@/components/ui/select'; +import { copyText } from '@/lib/clipboard'; + +const ALERT_WEBHOOK_TYPE_OPTIONS = [ + { value: 'json', label: 'Generic JSON webhook' }, + { value: 'slack', label: 'Slack incoming webhook' }, +]; export default function SettingsAdminPage() { - const { token, user } = useAuth(); + const { token, user, deploymentMode } = useAuth(); + const isCloud = deploymentMode === 'cloud'; // SMTP const [smtpHost, setSmtpHost] = useState(''); @@ -24,6 +32,19 @@ export default function SettingsAdminPage() { const [footerLinks, setFooterLinks] = useState>([]); const [footerMsg, setFooterMsg] = useState(''); + // Connector-failure alert webhook + const [alertUrl, setAlertUrl] = useState(''); + const [alertType, setAlertType] = useState<'json' | 'slack'>('json'); + const [alertEnabled, setAlertEnabled] = useState(true); + const [alertThreshold, setAlertThreshold] = useState(5); + const [alertWindowMinutes, setAlertWindowMinutes] = useState(10); + const [alertCooldownMinutes, setAlertCooldownMinutes] = useState(30); + const [alertConfigured, setAlertConfigured] = useState(false); + const [alertRotateSecret, setAlertRotateSecret] = useState(false); + const [alertMsg, setAlertMsg] = useState(''); + const [alertSecret, setAlertSecret] = useState(''); + const [alertSecretCopied, setAlertSecretCopied] = useState(false); + useEffect(() => { if (!token) return; @@ -37,6 +58,17 @@ export default function SettingsAdminPage() { }).catch(() => {}); adminSettings.getFooterLinks(token).then(setFooterLinks).catch(() => {}); + + adminSettings.getAlertWebhook(token).then((data) => { + if (!('url' in data)) return; + setAlertConfigured(true); + setAlertUrl(data.url); + setAlertType(data.type); + setAlertEnabled(data.enabled); + setAlertThreshold(data.threshold); + setAlertWindowMinutes(data.windowMinutes); + setAlertCooldownMinutes(data.cooldownMinutes); + }).catch(() => {}); }, [token]); const handleSaveSmtp = async () => { @@ -88,6 +120,70 @@ export default function SettingsAdminPage() { } }; + const handleSaveAlertWebhook = async () => { + if (!token) return; + const data: AlertWebhookInput = { + url: alertUrl, + type: alertType, + enabled: alertEnabled, + threshold: alertThreshold, + windowMinutes: alertWindowMinutes, + cooldownMinutes: alertCooldownMinutes, + rotateSecret: alertRotateSecret, + }; + try { + const result = await adminSettings.updateAlertWebhook(data, token); + setAlertConfigured(true); + setAlertRotateSecret(false); + setAlertMsg('Alert webhook saved'); + setTimeout(() => setAlertMsg(''), 3000); + if (result.secret) { + setAlertSecret(result.secret); + setAlertSecretCopied(false); + } + } catch (err: any) { + setAlertMsg(`Error: ${err.message}`); + } + }; + + const handleTestAlertWebhook = async () => { + if (!token) return; + setAlertMsg('Sending test alert...'); + try { + const result = await adminSettings.testAlertWebhook(token); + setAlertMsg(result.success ? 'Test alert delivered' : `Error: ${result.message || 'delivery failed'}`); + } catch (err: any) { + setAlertMsg(`Error: ${err.message}`); + } + }; + + const handleRemoveAlertWebhook = async () => { + if (!token) return; + if (!confirm('Remove the connector-failure alert webhook? Alerts will stop firing for this organization.')) return; + try { + await adminSettings.deleteAlertWebhook(token); + setAlertConfigured(false); + setAlertUrl(''); + setAlertType('json'); + setAlertEnabled(true); + setAlertThreshold(5); + setAlertWindowMinutes(10); + setAlertCooldownMinutes(30); + setAlertSecret(''); + setAlertMsg('Alert webhook removed'); + } catch (err: any) { + setAlertMsg(`Error: ${err.message}`); + } + }; + + const handleCopyAlertSecret = async () => { + const ok = await copyText(alertSecret); + if (ok) { + setAlertSecretCopied(true); + setTimeout(() => setAlertSecretCopied(false), 2000); + } + }; + const handleSaveFooterLinks = async () => { if (!token) return; try { @@ -211,6 +307,139 @@ export default function SettingsAdminPage() { + {/* Connector Failure Alerts */} + +

Connector Failure Alerts

+

+ Get a webhook or Slack notification when a connector keeps failing, instead of finding out from the logs. +

+
+
+ + setAlertUrl(e.target.value)} + placeholder="https://example.com/hooks/anythingmcp" + className={inputClass} + /> + {isCloud && ( +

On AnythingMCP Cloud the URL must use https.

+ )} +
+
+
+ + setAlertType(v as 'json' | 'slack')} + options={ALERT_WEBHOOK_TYPE_OPTIONS} + className={inputClass} + aria-label="Payload type" + /> +
+
+ setAlertEnabled(e.target.checked)} + className="accent-[var(--brand)]" + /> + +
+
+
+
+ + setAlertThreshold(Number(e.target.value))} + className={inputClass} + /> +
+
+ + setAlertWindowMinutes(Number(e.target.value))} + className={inputClass} + /> +
+
+ + setAlertCooldownMinutes(Number(e.target.value))} + className={inputClass} + /> +
+
+

+ Alerts after {alertThreshold} failures of the same connector within {alertWindowMinutes} minutes, then waits {alertCooldownMinutes} minutes before alerting again. +

+ {alertConfigured && ( +
+ setAlertRotateSecret(e.target.checked)} + className="accent-[var(--brand)]" + /> + +
+ )} + {alertSecret && ( +
+

+ Signing secret — shown once, copy it now: +

+
+ {alertSecret} + +
+

+ Use this to verify the X-AnythingMCP-Signature header. It will not be shown again. +

+
+ )} + {alertMsg && ( +

+ {alertMsg} +

+ )} +
+ + {alertConfigured && ( + <> + + + + )} +
+
+
+ {/* Footer Links */}

Footer Links

diff --git a/packages/frontend/src/lib/api.ts b/packages/frontend/src/lib/api.ts index e649ca10..ab2b4244 100644 --- a/packages/frontend/src/lib/api.ts +++ b/packages/frontend/src/lib/api.ts @@ -1031,8 +1031,38 @@ export const adminSettings = { request<{ hosts: string[]; envHosts: string[] }>('/api/admin/settings/ssrf-allowed-hosts', { token }), setSsrfAllowedHosts: (hosts: string[], token: string) => request<{ hosts: string[] }>('/api/admin/settings/ssrf-allowed-hosts', { method: 'PUT', body: { hosts }, token }), + // Connector-failure alert webhook. GET returns `{ configured: false }` when + // unset, or the config fields directly (no wrapper) when set — callers + // check for `url` rather than a `configured` flag that isn't always there. + getAlertWebhook: (token: string) => + request('/api/admin/settings/alert-webhook', { token }), + updateAlertWebhook: (data: AlertWebhookInput, token: string) => + request<{ message: string; secret?: string }>('/api/admin/settings/alert-webhook', { method: 'PUT', body: data, token }), + deleteAlertWebhook: (token: string) => + request<{ message: string }>('/api/admin/settings/alert-webhook', { method: 'DELETE', token }), + testAlertWebhook: (token: string) => + request<{ success: boolean; status?: number; message?: string }>('/api/admin/settings/alert-webhook/test', { method: 'POST', token }), }; +export interface AlertWebhookConfig { + url: string; + type: 'json' | 'slack'; + enabled: boolean; + threshold: number; + windowMinutes: number; + cooldownMinutes: number; +} + +export interface AlertWebhookInput { + url: string; + type: 'json' | 'slack'; + enabled?: boolean; + threshold?: number; + windowMinutes?: number; + cooldownMinutes?: number; + rotateSecret?: boolean; +} + // Roles (Admin) export interface IdentityProvider { id: string;