import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { apiFetch, createEventSource } from "./api"; import AdminPanel from "./admin"; import { useToast } from "./toast"; const languages = [ { code: "de", label: "Deutsch" }, { code: "en", label: "English" } ]; type Account = { id: string; email: string; provider: string; oauthConnected?: boolean; oauthExpiresAt?: string | null; oauthHealthy?: boolean; oauthError?: { code: string; message: string }; }; type Rule = { id: string; name: string; enabled: boolean; conditions: { type: string; value: string }[]; actions: { type: string; target?: string | null }[]; }; type Job = { id: string; status: string; createdAt: string; mailboxAccountId: string; dryRun: boolean; }; type JobEvent = { id: string; level: string; message: string; progress?: number | null; createdAt: string; }; const defaultCondition = { type: "LIST_UNSUBSCRIBE", value: "" }; const defaultAction = { type: "MOVE", target: "Newsletter" }; export default function App() { const { t, i18n } = useTranslation(); const { pushToast } = useToast(); const [activeLang, setActiveLang] = useState(i18n.language); const [token, setToken] = useState(localStorage.getItem("token") ?? ""); const [showAdmin, setShowAdmin] = useState( localStorage.getItem("ui.showAdmin") === "true" ); const [authMode, setAuthMode] = useState<"login" | "register">( (localStorage.getItem("ui.authMode") as "login" | "register") ?? "login" ); const [authEmail, setAuthEmail] = useState(""); const [authPassword, setAuthPassword] = useState(""); const [tenantName, setTenantName] = useState(""); const [user, setUser] = useState<{ email: string; role?: string } | null>(null); const [tenant, setTenant] = useState<{ name: string } | null>(null); const [accounts, setAccounts] = useState([]); const [rules, setRules] = useState([]); const [jobs, setJobs] = useState([]); const [selectedJobId, setSelectedJobId] = useState( localStorage.getItem("ui.selectedJobId") ); const [events, setEvents] = useState([]); const [accountEmail, setAccountEmail] = useState(""); const [accountProvider, setAccountProvider] = useState("GMAIL"); const [accountPassword, setAccountPassword] = useState(""); const [showProviderHelp, setShowProviderHelp] = useState(false); const [ruleName, setRuleName] = useState(""); const [ruleEnabled, setRuleEnabled] = useState(true); const [conditions, setConditions] = useState([{ ...defaultCondition }]); const [actions, setActions] = useState([{ ...defaultAction }]); const [cleanupAccountId, setCleanupAccountId] = useState(""); const [dryRun, setDryRun] = useState(true); const [unsubscribeEnabled, setUnsubscribeEnabled] = useState(true); const [routingEnabled, setRoutingEnabled] = useState(true); const switchLanguage = (code: string) => { i18n.changeLanguage(code); setActiveLang(code); }; const isAuthenticated = useMemo(() => Boolean(token), [token]); const getErrorMessage = (err: unknown) => { if (err instanceof Error) { try { const parsed = JSON.parse(err.message) as { message?: string }; if (parsed?.message) return parsed.message; } catch { // ignore parsing } return err.message; } return t("toastGenericError"); }; const loadInitial = async (authToken: string) => { const me = await apiFetch("/tenants/me", {}, authToken); setUser(me.user); if (me.user?.role === "ADMIN") { const stored = localStorage.getItem("ui.showAdmin"); if (stored === null) { setShowAdmin(false); } } setTenant(me.tenant); const accountsData = await apiFetch("/mail/accounts", {}, authToken); const enriched = await Promise.all((accountsData.accounts ?? []).map(async (account: Account) => { if (account.provider === "GMAIL") { try { const status = await apiFetch(`/oauth/gmail/ping/${account.id}`, {}, authToken); return { ...account, oauthConnected: status.connected, oauthHealthy: status.healthy, oauthExpiresAt: status.expiresAt, oauthError: status.error }; } catch { return { ...account, oauthConnected: false, oauthHealthy: false }; } } return account; })); setAccounts(enriched); if (!cleanupAccountId && accountsData.accounts?.length) { setCleanupAccountId(accountsData.accounts[0].id); } const rulesData = await apiFetch("/rules", {}, authToken); setRules(rulesData.rules ?? []); const jobsData = await apiFetch("/jobs", {}, authToken); setJobs(jobsData.jobs ?? []); }; useEffect(() => { if (!token) return; loadInitial(token).catch((err: unknown) => { const status = (err as { status?: number }).status; if (status === 401 || status === 403) { setToken(""); localStorage.removeItem("token"); pushToast(t("toastSessionExpired"), "info"); } }); }, [token]); useEffect(() => { localStorage.setItem("ui.showAdmin", String(showAdmin)); }, [showAdmin]); useEffect(() => { localStorage.setItem("ui.authMode", authMode); }, [authMode]); useEffect(() => { if (selectedJobId) { localStorage.setItem("ui.selectedJobId", selectedJobId); } else { localStorage.removeItem("ui.selectedJobId"); } }, [selectedJobId]); useEffect(() => { if (!token) return; const interval = setInterval(() => { loadInitial(token).catch(() => undefined); }, 30000); return () => clearInterval(interval); }, [token]); useEffect(() => { if (!token) return; const params = new URLSearchParams(window.location.search); if (window.location.pathname === "/oauth-success" || params.get("oauth") === "success") { window.history.replaceState({}, "", "/"); loadInitial(token).catch(() => undefined); } }, [token]); useEffect(() => { if (!selectedJobId || !token) return; apiFetch(`/jobs/${selectedJobId}/events`, {}, token) .then((data) => setEvents(data.events ?? [])) .catch(() => setEvents([])); const source = createEventSource(selectedJobId, token); source.onmessage = (event) => { const data = JSON.parse(event.data) as JobEvent; setEvents((prev) => [...prev, data]); }; return () => source.close(); }, [selectedJobId, token]); const handleAuth = async () => { try { if (authMode === "login") { const result = await apiFetch( "/auth/login", { method: "POST", body: JSON.stringify({ email: authEmail, password: authPassword }) } ); localStorage.setItem("token", result.token); setToken(result.token); pushToast(t("toastLoginSuccess"), "success"); return; } const result = await apiFetch( "/auth/register", { method: "POST", body: JSON.stringify({ tenantName, email: authEmail, password: authPassword }) } ); localStorage.setItem("token", result.token); setToken(result.token); pushToast(t("toastRegisterSuccess"), "success"); } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const handleAddAccount = async () => { try { const result = await apiFetch( "/mail/accounts", { method: "POST", body: JSON.stringify({ email: accountEmail, provider: accountProvider, appPassword: accountPassword || undefined }) }, token ); setAccounts((prev) => [...prev, result.account]); setAccountEmail(""); setAccountPassword(""); pushToast(t("toastMailboxAdded"), "success"); } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const handleAddRule = async () => { try { const result = await apiFetch( "/rules", { method: "POST", body: JSON.stringify({ name: ruleName, enabled: ruleEnabled, conditions, actions }) }, token ); setRules((prev) => [...prev, result.rule]); setRuleName(""); setConditions([{ ...defaultCondition }]); setActions([{ ...defaultAction }]); pushToast(t("toastRuleSaved"), "success"); } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const handleDeleteRule = async (ruleId: string) => { try { await apiFetch(`/rules/${ruleId}`, { method: "DELETE" }, token); setRules((prev) => prev.filter((rule) => rule.id !== ruleId)); pushToast(t("toastRuleDeleted"), "info"); } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const handleStartCleanup = async () => { try { const result = await apiFetch( "/mail/cleanup", { method: "POST", body: JSON.stringify({ mailboxAccountId: cleanupAccountId, dryRun, unsubscribeEnabled, routingEnabled }) }, token ); const jobsData = await apiFetch("/jobs", {}, token); setJobs(jobsData.jobs ?? []); setSelectedJobId(result.jobId); setEvents([]); pushToast(t("toastCleanupStarted"), "success"); } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const handleLogout = () => { setToken(""); localStorage.removeItem("token"); setUser(null); setTenant(null); pushToast(t("toastLoggedOut"), "info"); }; const addCondition = () => setConditions((prev) => [...prev, { ...defaultCondition }]); const addAction = () => setActions((prev) => [...prev, { ...defaultAction }]); const mapJobStatus = (status: string) => { switch (status) { case "RUNNING": return t("statusRunning"); case "QUEUED": return t("statusQueued"); case "SUCCEEDED": return t("statusSucceeded"); case "FAILED": return t("statusFailed"); case "CANCELED": return t("statusCanceled"); default: return status; } }; const handleImpersonate = (impersonationToken: string) => { localStorage.setItem("token", impersonationToken); setToken(impersonationToken); }; const startGmailOauth = async (accountId: string) => { try { const result = await apiFetch( "/oauth/gmail/url", { method: "POST", body: JSON.stringify({ accountId }) }, token ); if (result.url) { window.location.href = result.url; } } catch (err) { pushToast(getErrorMessage(err), "error"); } }; const providerHint = () => { if (accountProvider === "GMAIL") return t("providerHintGmail"); if (accountProvider === "GMX") return t("providerHintGmx"); return t("providerHintWebde"); }; if (!isAuthenticated) { return (

v0.1

{t("appName")}

{t("tagline")}

{languages.map((lang) => ( ))}

{authMode === "login" ? t("login") : t("register")}

{t("description")}

{authMode === "register" && ( setTenantName(event.target.value)} /> )} setAuthEmail(event.target.value)} /> setAuthPassword(event.target.value)} />
); } return (

v0.1

{t("appName")}

{tenant?.name ?? t("tenantFallback")}

{user?.email ?? ""}
{languages.map((lang) => ( ))}

{t("welcome")}

{t("description")}

{t("progress")} {events.at(-1)?.progress ?? 0}%

{t("mailboxes")}

{accounts.length}

{t("jobs")}

{jobs.length}

{t("rules")}

{rules.length}

{t("progressNote")}

{user?.role === "ADMIN" && (
{t("admin")}
)} {user?.role === "ADMIN" && showAdmin ? (

{t("adminConsole")}

{t("adminConsoleHint")}

) : (

{t("userWorkspace")}

{t("userWorkspaceHint")}

)} {!showAdmin && (

{t("mailboxAdd")}

setAccountEmail(event.target.value)} /> setAccountPassword(event.target.value)} />

{providerHint()}

{accountProvider === "GMAIL" && cleanupAccountId && ( )}

{t("cleanupStart")}

{t("rulesTitle")}

setRuleName(event.target.value)} />

{t("rulesConditions")}

{conditions.map((condition, idx) => (
setConditions((prev) => prev.map((item, index) => index === idx ? { ...item, value: event.target.value } : item ) ) } />
))}

{t("rulesActions")}

{actions.map((action, idx) => (
setActions((prev) => prev.map((item, index) => index === idx ? { ...item, target: event.target.value } : item ) ) } />
))}
)} {!showAdmin && (

{t("adminMailboxStatus")}

{accounts.map((account) => (
{account.email} {account.provider === "GMAIL" && ( {account.oauthConnected ? t("badgeConnected") : t("badgeMissing")} )}

{account.provider === "GMAIL" ? t("providerGmail") : account.provider === "GMX" ? t("providerGmx") : t("providerWebde")} {account.provider === "GMAIL" ? ` · ${account.oauthConnected ? t("oauthConnected") : t("oauthMissing")}` : ""}

{account.provider === "GMAIL" && (

{t("statusLabel")}: {account.oauthHealthy ? t("badgeHealthy") : t("badgeUnhealthy")} ·{" "} {t("adminExpiresAt")}: {account.oauthExpiresAt ? new Date(account.oauthExpiresAt).toLocaleString() : t("oauthStatusUnknown")}

)} {account.provider === "GMAIL" && account.oauthError && (

{account.oauthError.code === "invalid_grant" ? t("oauthErrorInvalidGrant") : account.oauthError.code === "token_expired" ? t("oauthErrorExpired") : t("oauthErrorUnknown")}

)}
{account.provider === "GMAIL" && ( )}
))}
)} {!showAdmin && (

{t("rulesOverview")}

{rules.map((rule) => (
{rule.name}

{t("ruleConditionsCount", { count: rule.conditions.length })} ·{" "} {t("ruleActionsCount", { count: rule.actions.length })}

))}

{t("jobsTitle")}

{jobs.map((job) => (
{mapJobStatus(job.status)}

{new Date(job.createdAt).toLocaleString()}

))}

{t("jobEvents")}

{selectedJobId ? (
{events.map((event) => (
{event.progress ?? "-"}%

{event.message}

))}
) : (

{t("noJobSelected")}

)}
)}
{showProviderHelp && (
setShowProviderHelp(false)}>
event.stopPropagation()}>

{t("providerHelpTitle")}

{t("providerGmail")}

{t("providerHelpGmail")}

{t("providerGmx")}

{t("providerHelpGmx")}

{t("providerWebde")}

{t("providerHelpWebde")}

)}
); }