diff --git a/apps/web/src/pages/SettingsPage.tsx b/apps/web/src/pages/SettingsPage.tsx index 8ef4a96..351a6a3 100644 --- a/apps/web/src/pages/SettingsPage.tsx +++ b/apps/web/src/pages/SettingsPage.tsx @@ -1,149 +1,177 @@ -import { useEffect, useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { Loader2, Play, KeyRound } from 'lucide-react'; -import { apiGet, apiPatch, apiPost } from '../lib/api'; -import { useAuth } from '../lib/auth'; -import type { Settings, JobStatus } from '../lib/types'; -import { PageHeader } from '../components/PageHeader'; -import { Button } from '../components/ui/Button'; -import { Input } from '../components/ui/Input'; -import { Label } from '../components/ui/Label'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../components/ui/Card'; -import { Alert } from '../components/ui/Alert'; -import { Skeleton } from '../components/ui/Skeleton'; +import { useEffect, useState } from "react" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { Loader2, Play, KeyRound } from "lucide-react" +import { apiGet, apiPatch, apiPost } from "../lib/api" +import { useAuth } from "../lib/auth" +import type { Settings, JobStatus } from "../lib/types" +import { PageHeader } from "../components/PageHeader" +import { Button } from "../components/ui/Button" +import { Input } from "../components/ui/Input" +import { Label } from "../components/ui/Label" +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "../components/ui/Card" +import { Alert } from "../components/ui/Alert" +import { Skeleton } from "../components/ui/Skeleton" const JOBS = [ - { id: 'dispatcher', label: 'Campaign Dispatcher', path: '/api/jobs/dispatcher/run' }, - { id: 'rollover', label: 'Campaign Rollover', path: '/api/jobs/rollover/run' }, - { id: 'rescuer', label: 'Spam Rescuer', path: '/api/jobs/rescuer/run' }, - { id: 'rollup', label: 'Stats Rollup', path: '/api/jobs/rollup/run' }, -] as const; + { + id: "dispatcher", + label: "Campaign Dispatcher", + path: "/api/jobs/dispatcher/run", + }, + { + id: "rollover", + label: "Campaign Rollover", + path: "/api/jobs/rollover/run", + }, + { id: "rescuer", label: "Spam Rescuer", path: "/api/jobs/rescuer/run" }, + { id: "rollup", label: "Stats Rollup", path: "/api/jobs/rollup/run" }, +] as const type PostmasterIntegrationStatus = { - configured: boolean; - connected: boolean; - domains: string[]; - message: string; - hasClientId: boolean; -}; + configured: boolean + connected: boolean + domains: string[] + message: string + hasClientId: boolean +} export function SettingsPage() { - const queryClient = useQueryClient(); - const { apiKey, baseUrl, login } = useAuth(); - const [localKey, setLocalKey] = useState(apiKey); - const [localUrl, setLocalUrl] = useState(baseUrl); - const [form, setForm] = useState({}); - const [message, setMessage] = useState(''); - const [error, setError] = useState(''); - const [runningJob, setRunningJob] = useState(null); + const queryClient = useQueryClient() + const { apiKey, baseUrl, login } = useAuth() + const [localKey, setLocalKey] = useState(apiKey) + const [localUrl, setLocalUrl] = useState(baseUrl) + const [form, setForm] = useState({}) + const [message, setMessage] = useState("") + const [error, setError] = useState("") + const [runningJob, setRunningJob] = useState(null) - const [googleClientId, setGoogleClientId] = useState(''); - const [googleClientSecret, setGoogleClientSecret] = useState(''); - const [microsoftClientId, setMicrosoftClientId] = useState(''); - const [microsoftClientSecret, setMicrosoftClientSecret] = useState(''); + const [googleClientId, setGoogleClientId] = useState("") + const [googleClientSecret, setGoogleClientSecret] = useState("") + const [microsoftClientId, setMicrosoftClientId] = useState("") + const [microsoftClientSecret, setMicrosoftClientSecret] = useState("") const { data: postmasterStatus, refetch: refetchPostmaster } = useQuery({ - queryKey: ['integrations', 'google', 'status'], - queryFn: () => apiGet('/api/integrations/google/status'), - }); + queryKey: ["integrations", "google", "status"], + queryFn: () => + apiGet("/api/integrations/google/status"), + }) const { data: microsoftStatus, refetch: refetchMicrosoft } = useQuery({ - queryKey: ['integrations', 'microsoft', 'status'], - queryFn: () => apiGet<{ configured: boolean; hasClientId: boolean }>( - '/api/integrations/microsoft/status', - ), - }); + queryKey: ["integrations", "microsoft", "status"], + queryFn: () => + apiGet<{ configured: boolean; hasClientId: boolean }>( + "/api/integrations/microsoft/status", + ), + }) const saveMicrosoftCredsMutation = useMutation({ mutationFn: () => - apiPost('/api/integrations/microsoft/credentials', { + apiPost("/api/integrations/microsoft/credentials", { microsoft_oauth_client_id: microsoftClientId, microsoft_oauth_client_secret: microsoftClientSecret, }), onSuccess: () => { - setMessage('Microsoft OAuth credentials saved'); - refetchMicrosoft(); + setMessage("Microsoft OAuth credentials saved") + refetchMicrosoft() }, onError: (err: Error) => setError(err.message), - }); + }) const saveGoogleCredsMutation = useMutation({ mutationFn: () => - apiPost('/api/integrations/google/credentials', { - google_oauth_client_id: googleClientId, - google_oauth_client_secret: googleClientSecret, - }), + apiPost( + "/api/integrations/google/credentials", + { + google_oauth_client_id: googleClientId, + google_oauth_client_secret: googleClientSecret, + }, + ), onSuccess: () => { - setMessage('Google OAuth credentials saved'); - refetchPostmaster(); + setMessage("Google OAuth credentials saved") + refetchPostmaster() }, onError: (err: Error) => setError(err.message), - }); + }) async function connectPostmaster() { - setError(''); + setError("") try { - const { url } = await apiGet<{ url: string }>('/api/integrations/google/auth-url'); - window.open(url, '_blank', 'noopener,noreferrer'); - setMessage('Complete Google authorization in the new tab, then refresh this page.'); + const { url } = await apiGet<{ url: string }>( + "/api/integrations/google/auth-url", + ) + window.open(url, "_blank", "noopener,noreferrer") + setMessage( + "Complete Google authorization in the new tab, then refresh this page.", + ) } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to start Google OAuth'); + setError( + err instanceof Error ? err.message : "Failed to start Google OAuth", + ) } } useEffect(() => { - if (window.location.hash === '#google-postmaster') { - document.getElementById('google-postmaster')?.scrollIntoView({ behavior: 'smooth' }); + if (window.location.hash === "#google-postmaster") { + document + .getElementById("google-postmaster") + ?.scrollIntoView({ behavior: "smooth" }) } - }, []); + }, []) const { data, isLoading } = useQuery({ - queryKey: ['settings'], - queryFn: () => apiGet('/api/settings'), - }); + queryKey: ["settings"], + queryFn: () => apiGet("/api/settings"), + }) const { data: jobStatus } = useQuery({ - queryKey: ['jobs', 'status'], - queryFn: () => apiGet<{ jobs: JobStatus[] }>('/api/jobs/status'), + queryKey: ["jobs", "status"], + queryFn: () => apiGet<{ jobs: JobStatus[] }>("/api/jobs/status"), refetchInterval: 30_000, - }); + }) useEffect(() => { - if (data) setForm(data); - }, [data]); + if (data) setForm(data) + }, [data]) const saveMutation = useMutation({ - mutationFn: (payload: Settings) => apiPatch('/api/settings', payload), + mutationFn: (payload: Settings) => + apiPatch("/api/settings", payload), onSuccess: (updated) => { - setForm(updated); - queryClient.setQueryData(['settings'], updated); - setMessage('Settings saved'); - setError(''); + setForm(updated) + queryClient.setQueryData(["settings"], updated) + setMessage("Settings saved") + setError("") }, onError: (err: Error) => setError(err.message), - }); + }) function handleSaveSettings(e: React.FormEvent) { - e.preventDefault(); - saveMutation.mutate(form); + e.preventDefault() + saveMutation.mutate(form) } function handleSaveApiConfig(e: React.FormEvent) { - e.preventDefault(); - login(localKey.trim(), localUrl.trim()); - setMessage('API configuration updated'); + e.preventDefault() + login(localKey.trim(), localUrl.trim()) + setMessage("API configuration updated") } async function runJob(path: string, id: string) { - setRunningJob(id); - setError(''); + setRunningJob(id) + setError("") try { - await apiPost(path); - setMessage(`Job "${id}" completed`); + await apiPost(path) + setMessage(`Job "${id}" completed`) } catch (err) { - setError(err instanceof Error ? err.message : 'Job failed'); + setError(err instanceof Error ? err.message : "Job failed") } finally { - setRunningJob(null); + setRunningJob(null) } } @@ -160,7 +188,9 @@ export function SettingsPage() { API Configuration - Connection settings for this dashboard session + + Connection settings for this dashboard session +
@@ -201,8 +231,10 @@ export function SettingsPage() { setForm({ ...form, dispatcher_cron: e.target.value })} + value={form.dispatcher_cron ?? ""} + onChange={(e) => + setForm({ ...form, dispatcher_cron: e.target.value }) + } placeholder="*/5 * * * *" /> @@ -210,8 +242,10 @@ export function SettingsPage() { setForm({ ...form, rescuer_cron: e.target.value })} + value={form.rescuer_cron ?? ""} + onChange={(e) => + setForm({ ...form, rescuer_cron: e.target.value }) + } placeholder="*/10 * * * *" /> @@ -219,28 +253,36 @@ export function SettingsPage() { setForm({ ...form, ramp_increment: e.target.value })} + value={form.ramp_increment ?? ""} + onChange={(e) => + setForm({ ...form, ramp_increment: e.target.value }) + } />
setForm({ ...form, ai_model: e.target.value })} + value={form.ai_model ?? ""} + onChange={(e) => + setForm({ ...form, ai_model: e.target.value }) + } />
setForm({ ...form, industries: e.target.value })} + value={form.industries ?? ""} + onChange={(e) => + setForm({ ...form, industries: e.target.value }) + } />
@@ -250,17 +292,18 @@ export function SettingsPage() { - Microsoft Outlook (@live.com) + Microsoft Outlook - Required for consumer Outlook accounts — app passwords no longer work for SMTP/IMAP + Required for consumer Outlook accounts — app passwords no longer + work for SMTP/IMAP {microsoftStatus && ( - + {microsoftStatus.configured - ? 'Microsoft OAuth credentials are configured. Connect mailboxes from Mailboxes → Add.' - : 'Add Azure app credentials below before connecting @live.com mailboxes.'} + ? "Microsoft OAuth credentials are configured. Connect mailboxes from Mailboxes → Add." + : "Add Azure app credentials below before connecting @live.com mailboxes."} )} @@ -268,7 +311,7 @@ export function SettingsPage() {

Setup (one-time)

  1. - In{' '} + In{" "} Azure App registrations - , create a Web app. Supported account types: personal Microsoft accounts. + , create a Web app. Supported account types: personal Microsoft + accounts.
  2. - Add redirect URI:{' '} + Add redirect URI:{" "} http://localhost:3000/api/integrations/microsoft/callback
  3. - Under API permissions, add delegated{' '} - IMAP.AccessAsUser.All and SMTP.Send for Office - 365 / Outlook. + Under API permissions, add delegated{" "} + IMAP.AccessAsUser.All and{" "} + SMTP.Send for Office 365 / Outlook. +
  4. +
  5. + Create a client secret, paste ID + secret below, then add + mailboxes via Connect Microsoft.
  6. -
  7. Create a client secret, paste ID + secret below, then add mailboxes via Connect Microsoft.
@@ -316,7 +363,9 @@ export function SettingsPage() { type="button" variant="outline" disabled={ - !microsoftClientId || !microsoftClientSecret || saveMicrosoftCredsMutation.isPending + !microsoftClientId || + !microsoftClientSecret || + saveMicrosoftCredsMutation.isPending } onClick={() => saveMicrosoftCredsMutation.mutate()} > @@ -330,16 +379,17 @@ export function SettingsPage() { Google Postmaster Tools - Connect once to auto-verify Gmail/Workspace domains in campaign compliance checks + Connect once to auto-verify Gmail/Workspace domains in campaign + compliance checks {postmasterStatus && ( - + {postmasterStatus.message} {postmasterStatus.domains.length > 0 && ( - Domains: {postmasterStatus.domains.join(', ')} + Domains: {postmasterStatus.domains.join(", ")} )} @@ -349,7 +399,7 @@ export function SettingsPage() {

Setup (one-time)

  1. - In{' '} + In{" "} Gmail Postmaster Tools API.
  2. - Create OAuth 2.0 credentials (Web application). Add redirect URI:{' '} + Create OAuth 2.0 credentials (Web application). Add redirect + URI:{" "} http://localhost:3000/api/integrations/google/callback
  3. - Add your sending domain at{' '} + Add your sending domain at{" "} postmaster.google.com - {' '} + {" "} and complete DNS verification there.
  4. -
  5. Paste Client ID and Secret below, save, then click Connect.
  6. +
  7. + Paste Client ID and Secret below, save, then click Connect. +
@@ -405,7 +458,11 @@ export function SettingsPage() {