From e1503632d9cd2853232f9c0900f40d2ad7e8b80d Mon Sep 17 00:00:00 2001 From: Stephan Noller Date: Mon, 13 Apr 2026 14:56:20 +0200 Subject: [PATCH 1/8] feat: add Fetch Email-Config button to admin email channel editor Adds a button that calls the HybridAI agent handles API to retrieve email configuration and auto-populate IMAP/SMTP fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- console/src/api/client.ts | 4 ++ console/src/routes/channels.tsx | 84 ++++++++++++++++++++++++++++++ src/gateway/gateway-http-server.ts | 67 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/console/src/api/client.ts b/console/src/api/client.ts index d5380a60..c9ddb2df 100644 --- a/console/src/api/client.ts +++ b/console/src/api/client.ts @@ -321,6 +321,10 @@ export function fetchConfig(token: string): Promise { return requestJson('/api/admin/config', { token }); } +export function fetchEmailConfig(token: string): Promise { + return requestJson('/api/admin/email-config/fetch', { token }); +} + export function saveConfig( token: string, config: AdminConfig, diff --git a/console/src/routes/channels.tsx b/console/src/routes/channels.tsx index 066680f7..f0913c0c 100644 --- a/console/src/routes/channels.tsx +++ b/console/src/routes/channels.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { fetchConfig, + fetchEmailConfig, saveConfig, setRuntimeSecret, validateToken, @@ -960,6 +961,78 @@ function EmailChannelEditor(props: { token: string; onSecretSaved: () => void; }) { + const [fetchingEmailConfig, setFetchingEmailConfig] = useState(false); + const toast = useToast(); + + async function handleFetchEmailConfig() { + setFetchingEmailConfig(true); + try { + const result = (await fetchEmailConfig(props.token)) as { + handles?: Array<{ + handle?: string; + label?: string; + status?: string; + email_config?: { + address?: string; + imap_host?: string; + imap_port?: number; + imap_secure?: boolean; + smtp_host?: string; + smtp_port?: number; + smtp_secure?: boolean; + password?: string; + }; + }>; + }; + + const handles = result?.handles; + if (!Array.isArray(handles) || handles.length === 0) { + toast.info('No agent handles found on HybridAI.'); + return; + } + + // Find first handle with email_config, or just show what we got + const withConfig = handles.find((h) => h.email_config); + if (withConfig?.email_config) { + const cfg = withConfig.email_config; + props.updateDraft((current) => ({ + ...current, + email: { + ...current.email, + ...(cfg.address ? { address: cfg.address } : {}), + ...(cfg.imap_host ? { imapHost: cfg.imap_host } : {}), + ...(cfg.imap_port != null ? { imapPort: cfg.imap_port } : {}), + ...(cfg.imap_secure != null ? { imapSecure: cfg.imap_secure } : {}), + ...(cfg.smtp_host ? { smtpHost: cfg.smtp_host } : {}), + ...(cfg.smtp_port != null ? { smtpPort: cfg.smtp_port } : {}), + ...(cfg.smtp_secure != null ? { smtpSecure: cfg.smtp_secure } : {}), + }, + })); + toast.success( + `Email config loaded from handle "${withConfig.handle}".`, + ); + // If a password was included, save it as runtime secret + if (cfg.password) { + await setRuntimeSecret(props.token, 'EMAIL_PASSWORD', cfg.password); + props.onSecretSaved(); + } + } else { + // No email_config in handles — show available handles info + const summary = handles + .map((h) => `${h.handle} (${h.status})`) + .join(', '); + toast.info(`Handles found: ${summary}. No email config attached.`); + } + } catch (error) { + toast.error( + 'Failed to fetch email config', + getErrorMessage(error), + ); + } finally { + setFetchingEmailConfig(false); + } + } + return ( <> +
+ +
+