diff --git a/src/app/(dashboard)/dashboard/a2a/page.tsx b/src/app/(dashboard)/dashboard/a2a/page.tsx index c21e1e8a8d..09ad34afb0 100644 --- a/src/app/(dashboard)/dashboard/a2a/page.tsx +++ b/src/app/(dashboard)/dashboard/a2a/page.tsx @@ -1,7 +1,193 @@ "use client"; +import { useState, useEffect, useCallback } from "react"; +import { Card } from "@/shared/components"; import A2ADashboardPage from "../endpoint/components/A2ADashboard"; +type ServiceStatus = { online: boolean; loading: boolean }; + +function ServiceToggle({ + label, + status, + enabled, + onToggle, + toggling, +}: { + label: string; + status: ServiceStatus; + enabled: boolean; + onToggle: () => void; + toggling: boolean; +}) { + const online = enabled && status.online; + const loading = enabled && status.loading; + + return ( +
+
+ + {loading ? "..." : online ? "Online" : "Offline"} +
+ + + + + {toggling ? "..." : enabled ? "ON" : "OFF"} + +
+ ); +} + +function DisabledPanel() { + return ( + +
+
+
+
+

+ A2A is disabled +

+

+ Enable A2A above to view task telemetry, agent details, and validation tools. +

+
+
+
+ ); +} + export default function A2APage() { - return ; + const [a2aStatus, setA2aStatus] = useState({ online: false, loading: true }); + const [a2aEnabled, setA2aEnabled] = useState(false); + const [a2aToggling, setA2aToggling] = useState(false); + + const patchSetting = useCallback(async (body: Record) => { + return fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + }, []); + + useEffect(() => { + const fetchSettings = async () => { + try { + const res = await fetch("/api/settings"); + if (res.ok) { + const data = await res.json(); + setA2aEnabled(!!data.a2aEnabled); + } + } catch { + // defaults stay + } + }; + void fetchSettings(); + }, []); + + const refreshStatus = useCallback(async () => { + setA2aStatus((prev) => ({ ...prev, loading: true })); + try { + const res = await fetch("/api/a2a/status"); + const data = res.ok ? await res.json() : null; + setA2aStatus({ online: data?.status === "ok", loading: false }); + } catch { + setA2aStatus({ online: false, loading: false }); + } + }, []); + + useEffect(() => { + void refreshStatus(); + const interval = setInterval(() => void refreshStatus(), 30000); + return () => clearInterval(interval); + }, [refreshStatus]); + + const toggleA2a = useCallback(async () => { + const newValue = !a2aEnabled; + setA2aToggling(true); + try { + const res = await patchSetting({ a2aEnabled: newValue }); + if (res.ok) setA2aEnabled(newValue); + } catch { + // keep current + } finally { + setA2aToggling(false); + } + }, [a2aEnabled, patchSetting]); + + return ( +
+
+ void toggleA2a()} + toggling={a2aToggling} + /> +
+ + {a2aEnabled ? : } +
+ ); } diff --git a/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx b/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx index 2ebd1fcfd1..7676419833 100644 --- a/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/ApiEndpointsTab.tsx @@ -25,19 +25,6 @@ interface CatalogData { schemas: string[]; } -interface WebhookItem { - id: string; - url: string; - events: string[]; - secret: string | null; - enabled: boolean; - description: string; - created_at: string; - last_triggered_at: string | null; - last_status: number | null; - failure_count: number; -} - interface TryItResult { status: number; statusText: string; @@ -55,22 +42,12 @@ const METHOD_COLORS: Record = { DELETE: "bg-red-500/15 text-red-500 border-red-500/30", }; -const WEBHOOK_EVENTS = [ - "request.completed", - "request.failed", - "provider.error", - "provider.recovered", - "quota.exceeded", - "combo.switched", -]; - /* ─── Main Component ─────────────────────────────────── */ export default function ApiEndpointsTab() { const baseUrl = useDisplayBaseUrl(); const [catalog, setCatalog] = useState(null); const [catalogError, setCatalogError] = useState(null); const [loading, setLoading] = useState(true); - const [section, setSection] = useState<"catalog" | "webhooks">("catalog"); const [search, setSearch] = useState(""); const [expandedEndpoint, setExpandedEndpoint] = useState(null); const [selectedTag, setSelectedTag] = useState(null); @@ -81,16 +58,6 @@ export default function ApiEndpointsTab() { const [tryResult, setTryResult] = useState(null); const [trying, setTrying] = useState(false); - // Webhooks state - const [webhooks, setWebhooks] = useState([]); - const [webhooksLoading, setWebhooksLoading] = useState(false); - const [showAddWebhook, setShowAddWebhook] = useState(false); - const [whUrl, setWhUrl] = useState(""); - const [whEvents, setWhEvents] = useState(["*"]); - const [whDesc, setWhDesc] = useState(""); - const [testingWebhookId, setTestingWebhookId] = useState(null); - - // Load catalog const loadCatalog = async () => { try { const res = await fetch("/api/openapi/spec"); @@ -124,39 +91,6 @@ export default function ApiEndpointsTab() { }; }, []); - // Load webhooks - const fetchWebhooksData = async (): Promise => { - try { - const res = await fetch("/api/webhooks"); - if (res.ok) { - const data = await res.json(); - return data.webhooks || []; - } - } catch {} - return []; - }; - - const loadWebhooks = async () => { - setWebhooksLoading(true); - const data = await fetchWebhooksData(); - setWebhooks(data); - setWebhooksLoading(false); - }; - - useEffect(() => { - if (section !== "webhooks") return; - let cancelled = false; - fetchWebhooksData().then((data) => { - if (!cancelled) { - setWebhooks(data); - setWebhooksLoading(false); - } - }); - return () => { - cancelled = true; - }; - }, [section]); - // Filter endpoints const filteredEndpoints = useMemo(() => { if (!catalog) return []; @@ -226,51 +160,6 @@ export default function ApiEndpointsTab() { setTrying(false); }; - // Webhook handlers - const addWebhook = async () => { - if (!whUrl.trim()) return; - try { - await fetch("/api/webhooks", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ url: whUrl, events: whEvents, description: whDesc }), - }); - setWhUrl(""); - setWhEvents(["*"]); - setWhDesc(""); - setShowAddWebhook(false); - await loadWebhooks(); - } catch {} - }; - - const toggleWebhook = async (wh: WebhookItem) => { - try { - await fetch(`/api/webhooks/${wh.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ enabled: !wh.enabled }), - }); - setWebhooks((prev) => prev.map((w) => (w.id === wh.id ? { ...w, enabled: !w.enabled } : w))); - } catch {} - }; - - const deleteWebhook = async (id: string) => { - if (!confirm("Delete this webhook?")) return; - try { - await fetch(`/api/webhooks/${id}`, { method: "DELETE" }); - setWebhooks((prev) => prev.filter((w) => w.id !== id)); - } catch {} - }; - - const testWebhook = async (id: string) => { - setTestingWebhookId(id); - try { - await fetch(`/api/webhooks/${id}/test`, { method: "POST" }); - await loadWebhooks(); - } catch {} - setTestingWebhookId(null); - }; - if (loading) { return (
@@ -327,30 +216,8 @@ export default function ApiEndpointsTab() { )} - {/* Section tabs */} -
- {[ - { id: "catalog" as const, label: "API Catalog", icon: "menu_book" }, - { id: "webhooks" as const, label: "Webhooks", icon: "webhook" }, - ].map((tab) => ( - - ))} -
- {/* ═══ API CATALOG ═══ */} - {section === "catalog" && !catalog && ( + {!catalog && (
@@ -376,7 +243,7 @@ export default function ApiEndpointsTab() { )} - {section === "catalog" && catalog && ( + {catalog && ( <> {/* Search & filter */}
@@ -647,232 +514,6 @@ export default function ApiEndpointsTab() { )} )} - - {/* ═══ WEBHOOKS ═══ */} - {section === "webhooks" && ( - <> - -
-
- webhook -
-

Event Webhooks

-

- Receive HTTP callbacks when events occur in OmniRoute -

-
-
- {!showAddWebhook && ( - - )} -
- - {/* Add webhook form */} - {showAddWebhook && ( -
-
-
- - setWhUrl(e.target.value)} - placeholder="https://example.com/webhook" - className="w-full mt-0.5 px-2.5 py-1.5 text-xs rounded-lg border border-black/10 dark:border-white/10 - bg-white dark:bg-black/20 focus:outline-none focus:ring-1 focus:ring-primary" - /> -
-
- - setWhDesc(e.target.value)} - placeholder="Production monitoring" - className="w-full mt-0.5 px-2.5 py-1.5 text-xs rounded-lg border border-black/10 dark:border-white/10 - bg-white dark:bg-black/20 focus:outline-none focus:ring-1 focus:ring-primary" - /> -
-
-
- -
- - {WEBHOOK_EVENTS.map((ev) => ( - - ))} -
-
-
- - -
-
- )} - - {/* Webhooks list */} - {webhooksLoading ? ( -
Loading...
- ) : webhooks.length === 0 ? ( -
- - webhook - -

- No webhooks configured. Add one to receive event notifications. -

-
- ) : ( -
- {webhooks.map((wh) => ( -
-
-
- {wh.url} - {wh.failure_count > 0 && ( - - {wh.failure_count} failures - - )} -
-
- {wh.description && ( - {wh.description} - )} - - Events: {wh.events.join(", ")} - - {wh.last_triggered_at && ( - - Last: {new Date(wh.last_triggered_at).toLocaleString()} - {wh.last_status ? ` (${wh.last_status})` : ""} - - )} -
-
-
- - - -
-
- ))} -
- )} -
- - {/* Webhook signature info */} - -
- vpn_key -

Webhook Signatures

-
-

- Each webhook delivery includes an{" "} - - X-Webhook-Signature - {" "} - header signed with HMAC-SHA256 using the webhook secret. Verify the signature to - ensure the payload is authentic. -

-
- - {`const crypto = require('crypto');\nconst sig = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');\nif (sig !== req.headers['x-webhook-signature']) throw new Error('Invalid signature');`} - -
-
- - )}
); } diff --git a/src/app/(dashboard)/dashboard/endpoint/page.tsx b/src/app/(dashboard)/dashboard/endpoint/page.tsx index 4d5d159e3e..740a86fbbc 100644 --- a/src/app/(dashboard)/dashboard/endpoint/page.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/page.tsx @@ -1,424 +1,5 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Card, SegmentedControl } from "@/shared/components"; import EndpointPageClient from "./EndpointPageClient"; -import McpDashboardPage from "./components/MCPDashboard"; -import A2ADashboardPage from "./components/A2ADashboard"; -import ApiEndpointsTab from "./ApiEndpointsTab"; -import { useTranslations } from "next-intl"; -import { copyToClipboard } from "@/shared/utils/clipboard"; -type ServiceStatus = { - online: boolean; - loading: boolean; -}; - -type McpTransport = "stdio" | "sse" | "streamable-http"; - -/* ────── Toggle Switch ────── */ -function ServiceToggle({ - label, - status, - enabled, - onToggle, - toggling, -}: { - label: string; - status: ServiceStatus; - enabled: boolean; - onToggle: () => void; - toggling: boolean; -}) { - const online = enabled && status.online; - const loading = enabled && status.loading; - - return ( -
-
- - {loading ? "..." : online ? "Online" : "Offline"} -
- - - - - {toggling ? "..." : enabled ? "ON" : "OFF"} - -
- ); -} - -function DisabledServicePanel({ title, description }: { title: string; description: string }) { - return ( - -
-
-
-
-

- {title} -

-

- {description} -

-
-
-
- ); -} - -/* ────── Transport Selector ────── */ -function TransportSelector({ - value, - onChange, - disabled, - baseUrl, -}: { - value: McpTransport; - onChange: (t: McpTransport) => void; - disabled: boolean; - baseUrl: string; -}) { - const options: { value: McpTransport; label: string; desc: string }[] = [ - { value: "stdio", label: "stdio", desc: "Local — IDE spawns process via omniroute --mcp" }, - { value: "sse", label: "SSE", desc: "Remote — Server-Sent Events over HTTP" }, - { - value: "streamable-http", - label: "Streamable HTTP", - desc: "Remote — Modern bidirectional HTTP", - }, - ]; - - const urlMap: Record = { - stdio: "omniroute --mcp", - sse: `${baseUrl}/api/mcp/sse`, - "streamable-http": `${baseUrl}/api/mcp/stream`, - }; - - return ( -
-
- - swap_horiz - - - Transport Mode - -
- -
- {options.map((opt) => ( - - ))} -
- - {/* Connection info */} -
- - {value === "stdio" ? "terminal" : "link"} - - - {urlMap[value]} - - {value !== "stdio" && ( - - )} -
-
- ); -} - -/* ────── Main Page ────── */ export default function EndpointPage() { - const [activeTab, setActiveTab] = useState("endpoint-proxy"); - const t = useTranslations("endpoints"); - - const [mcpStatus, setMcpStatus] = useState({ online: false, loading: true }); - const [a2aStatus, setA2aStatus] = useState({ online: false, loading: true }); - const [mcpEnabled, setMcpEnabled] = useState(false); - const [a2aEnabled, setA2aEnabled] = useState(false); - const [mcpToggling, setMcpToggling] = useState(false); - const [a2aToggling, setA2aToggling] = useState(false); - const [mcpTransport, setMcpTransport] = useState("stdio"); - const [transportSaving, setTransportSaving] = useState(false); - - const [baseUrl, setBaseUrl] = useState(""); - - // Detect base URL from browser - useEffect(() => { - if (typeof window !== "undefined") { - setBaseUrl(`${window.location.protocol}//${window.location.host}`); - } - }, []); - - // Fetch initial settings - useEffect(() => { - const fetchSettings = async () => { - try { - const res = await fetch("/api/settings"); - if (res.ok) { - const data = await res.json(); - setMcpEnabled(!!data.mcpEnabled); - setA2aEnabled(!!data.a2aEnabled); - setMcpTransport((data.mcpTransport as McpTransport) || "stdio"); - } - } catch { - // defaults stay - } - }; - void fetchSettings(); - }, []); - - const patchSetting = useCallback(async (body: Record) => { - return fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - }, []); - - const toggleService = useCallback( - async (service: "mcp" | "a2a") => { - const setToggling = service === "mcp" ? setMcpToggling : setA2aToggling; - const setEnabled = service === "mcp" ? setMcpEnabled : setA2aEnabled; - const currentlyEnabled = service === "mcp" ? mcpEnabled : a2aEnabled; - const newValue = !currentlyEnabled; - - setToggling(true); - try { - const res = await patchSetting({ - [service === "mcp" ? "mcpEnabled" : "a2aEnabled"]: newValue, - }); - if (res.ok) setEnabled(newValue); - } catch { - // keep current state - } finally { - setToggling(false); - } - }, - [mcpEnabled, a2aEnabled, patchSetting] - ); - - const changeTransport = useCallback( - async (newTransport: McpTransport) => { - setTransportSaving(true); - try { - const res = await patchSetting({ mcpTransport: newTransport }); - if (res.ok) setMcpTransport(newTransport); - } catch { - // keep current - } finally { - setTransportSaving(false); - } - }, - [patchSetting] - ); - - const refreshMcpStatus = useCallback(async () => { - setMcpStatus((prev) => ({ ...prev, loading: true })); - try { - const res = await fetch("/api/mcp/status"); - if (res.ok) { - const data = await res.json(); - setMcpStatus({ online: !!data.online, loading: false }); - } else { - setMcpStatus({ online: false, loading: false }); - } - } catch { - setMcpStatus({ online: false, loading: false }); - } - }, []); - - const refreshA2aStatus = useCallback(async () => { - setA2aStatus((prev) => ({ ...prev, loading: true })); - try { - const res = await fetch("/api/a2a/status"); - if (res.ok) { - const data = await res.json(); - setA2aStatus({ online: data.status === "ok", loading: false }); - } else { - setA2aStatus({ online: false, loading: false }); - } - } catch { - setA2aStatus({ online: false, loading: false }); - } - }, []); - - useEffect(() => { - const load = () => { - void refreshMcpStatus(); - void refreshA2aStatus(); - }; - load(); - const interval = setInterval(load, 30000); - return () => clearInterval(interval); - }, [refreshMcpStatus, refreshA2aStatus]); - - return ( -
-
- - - {activeTab === "mcp" && ( - void toggleService("mcp")} - toggling={mcpToggling} - /> - )} - {activeTab === "a2a" && ( - void toggleService("a2a")} - toggling={a2aToggling} - /> - )} -
- - {/* Transport selector for MCP */} - {activeTab === "mcp" && mcpEnabled && ( - void changeTransport(t)} - disabled={transportSaving} - baseUrl={baseUrl} - /> - )} - - {activeTab === "endpoint-proxy" && } - {activeTab === "mcp" && } - {activeTab === "a2a" && - (a2aEnabled ? ( - - ) : ( - - ))} - {activeTab === "api-endpoints" && } -
- ); + return ; } diff --git a/src/app/(dashboard)/dashboard/mcp/page.tsx b/src/app/(dashboard)/dashboard/mcp/page.tsx index 68349a0792..aa7aad0e58 100644 --- a/src/app/(dashboard)/dashboard/mcp/page.tsx +++ b/src/app/(dashboard)/dashboard/mcp/page.tsx @@ -1,7 +1,332 @@ "use client"; +import { useState, useEffect, useCallback } from "react"; +import { Card } from "@/shared/components"; +import { copyToClipboard } from "@/shared/utils/clipboard"; import McpDashboardPage from "../endpoint/components/MCPDashboard"; +type ServiceStatus = { online: boolean; loading: boolean }; +type McpTransport = "stdio" | "sse" | "streamable-http"; + +function ServiceToggle({ + label, + status, + enabled, + onToggle, + toggling, +}: { + label: string; + status: ServiceStatus; + enabled: boolean; + onToggle: () => void; + toggling: boolean; +}) { + const online = enabled && status.online; + const loading = enabled && status.loading; + + return ( +
+
+ + {loading ? "..." : online ? "Online" : "Offline"} +
+ + + + + {toggling ? "..." : enabled ? "ON" : "OFF"} + +
+ ); +} + +function TransportSelector({ + value, + onChange, + disabled, + baseUrl, +}: { + value: McpTransport; + onChange: (t: McpTransport) => void; + disabled: boolean; + baseUrl: string; +}) { + const options: { value: McpTransport; label: string; desc: string }[] = [ + { value: "stdio", label: "stdio", desc: "Local — IDE spawns process via omniroute --mcp" }, + { value: "sse", label: "SSE", desc: "Remote — Server-Sent Events over HTTP" }, + { + value: "streamable-http", + label: "Streamable HTTP", + desc: "Remote — Modern bidirectional HTTP", + }, + ]; + + const urlMap: Record = { + stdio: "omniroute --mcp", + sse: `${baseUrl}/api/mcp/sse`, + "streamable-http": `${baseUrl}/api/mcp/stream`, + }; + + return ( +
+
+ + swap_horiz + + + Transport Mode + +
+ +
+ {options.map((opt) => ( + + ))} +
+ +
+ + {value === "stdio" ? "terminal" : "link"} + + + {urlMap[value]} + + {value !== "stdio" && ( + + )} +
+
+ ); +} + +function DisabledPanel() { + return ( + +
+
+
+
+

+ MCP is disabled +

+

+ Enable MCP above to configure transport mode and view server telemetry. +

+
+
+
+ ); +} + export default function McpPage() { - return ; + const [mcpStatus, setMcpStatus] = useState({ online: false, loading: true }); + const [mcpEnabled, setMcpEnabled] = useState(false); + const [mcpToggling, setMcpToggling] = useState(false); + const [mcpTransport, setMcpTransport] = useState("stdio"); + const [transportSaving, setTransportSaving] = useState(false); + const [baseUrl, setBaseUrl] = useState(""); + + useEffect(() => { + if (typeof window !== "undefined") { + setBaseUrl(`${window.location.protocol}//${window.location.host}`); + } + }, []); + + const patchSetting = useCallback(async (body: Record) => { + return fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + }, []); + + useEffect(() => { + const fetchSettings = async () => { + try { + const res = await fetch("/api/settings"); + if (res.ok) { + const data = await res.json(); + setMcpEnabled(!!data.mcpEnabled); + setMcpTransport((data.mcpTransport as McpTransport) || "stdio"); + } + } catch { + // defaults stay + } + }; + void fetchSettings(); + }, []); + + const refreshStatus = useCallback(async () => { + setMcpStatus((prev) => ({ ...prev, loading: true })); + try { + const res = await fetch("/api/mcp/status"); + setMcpStatus({ online: res.ok ? !!(await res.json()).online : false, loading: false }); + } catch { + setMcpStatus({ online: false, loading: false }); + } + }, []); + + useEffect(() => { + void refreshStatus(); + const interval = setInterval(() => void refreshStatus(), 30000); + return () => clearInterval(interval); + }, [refreshStatus]); + + const toggleMcp = useCallback(async () => { + const newValue = !mcpEnabled; + setMcpToggling(true); + try { + const res = await patchSetting({ mcpEnabled: newValue }); + if (res.ok) setMcpEnabled(newValue); + } catch { + // keep current + } finally { + setMcpToggling(false); + } + }, [mcpEnabled, patchSetting]); + + const changeTransport = useCallback( + async (newTransport: McpTransport) => { + setTransportSaving(true); + try { + const res = await patchSetting({ mcpTransport: newTransport }); + if (res.ok) setMcpTransport(newTransport); + } catch { + // keep current + } finally { + setTransportSaving(false); + } + }, + [patchSetting] + ); + + return ( +
+
+ void toggleMcp()} + toggling={mcpToggling} + /> +
+ + {mcpEnabled && ( + void changeTransport(t)} + disabled={transportSaving} + baseUrl={baseUrl} + /> + )} + + {mcpEnabled ? : } +
+ ); }