From aa0207436cf303623fc446d1dc6b880aabdc1bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 4 Mar 2026 23:13:55 +0000 Subject: [PATCH] improve gmail sync: email input, permanent errors, auto-dock - add isync to setup.sh - ask for gmail address alongside app password in integrations - add PermanentError to job queue (skips retries for non-recoverable failures) - use PermanentError for missing credentials, missing executable, auth failures - auto-add /email to dock after successful gmail sync - invalidate dock cache on sync completion for seamless UI update Co-Authored-By: Claude Opus 4.6 --- scripts/setup.sh | 9 ++ .../IntegrationsSettings/GoogleAccount.tsx | 84 ++++++++++++++----- src/servers/api/integrations/integrations.ts | 9 +- src/servers/queue/engine.ts | 19 +++-- src/servers/queue/handlers/gmail-sync.ts | 48 ++++++++--- src/servers/queue/index.ts | 12 ++- src/servers/queue/types.ts | 8 ++ 7 files changed, 146 insertions(+), 43 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 955cd265..d1c2eb42 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -135,6 +135,15 @@ if has sqlite3; then skip "sqlite3"; else esac fi +# isync (provides mbsync for Gmail IMAP sync) +if has mbsync; then skip "isync (mbsync)"; else + case $PM in + apt) CORE_PKGS+=(isync) ;; + pacman) CORE_PKGS+=(isync) ;; + brew) CORE_PKGS+=(isync) ;; + esac +fi + # ripgrep if has rg; then skip "ripgrep"; else case $PM in diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleAccount.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleAccount.tsx index cf0f32de..992b5a15 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleAccount.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleAccount.tsx @@ -3,6 +3,8 @@ import { toast } from 'sonner'; import { RefreshCw, Loader2, CheckCircle2, XCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; +import { useAuth } from 'hooks/useAuth'; +import { useQueryClient } from '@tanstack/react-query'; import { useJobs } from 'hooks/useJobs'; type GoogleStatus = { @@ -20,8 +22,17 @@ const formatTime = (ts: number | string) => { export const GoogleAccount = () => { const client = useClient(); + const { user } = useAuth(); + const queryClient = useQueryClient(); const [isLoading, setIsLoading] = useState(true); - const [status, setStatus] = useState({ connected: false, email: null, picture: null, configured: false, hasAppPassword: false }); + const [status, setStatus] = useState({ + connected: false, + email: null, + picture: null, + configured: false, + hasAppPassword: false, + }); + const [gmailEmail, setGmailEmail] = useState(''); const [appPassword, setAppPassword] = useState(''); const [showPasswordInput, setShowPasswordInput] = useState(false); const [savingPassword, setSavingPassword] = useState(false); @@ -34,7 +45,10 @@ export const GoogleAccount = () => { const fetchStatus = () => { client .get('/integrations/google/status') - .then(setStatus) + .then((s) => { + setStatus(s); + setGmailEmail(s.email ?? user?.email ?? ''); + }) .catch(() => {}) .finally(() => setIsLoading(false)); client @@ -57,10 +71,14 @@ export const GoogleAccount = () => { } }, []); - // Refresh sync status from DB when a job finishes + // Refresh sync status and dock when a job finishes useEffect(() => { if (!activeJob && lastJob?.status === 'completed') { - client.get<{ lastSyncAt: string | null }>('/email/sync-status').then((res) => setLastSyncAt(res.lastSyncAt)).catch(() => {}); + client + .get<{ lastSyncAt: string | null }>('/email/sync-status') + .then((res) => setLastSyncAt(res.lastSyncAt)) + .catch(() => {}); + queryClient.invalidateQueries({ queryKey: ['DOCK'] }); } }, [activeJob, lastJob?.status]); @@ -93,11 +111,14 @@ export const GoogleAccount = () => { }; const handleSaveAppPassword = async () => { - if (!appPassword.trim()) return; + if (!appPassword.trim() || !gmailEmail.trim()) return; setSavingPassword(true); try { - await client.put('/integrations/google/app-password', { appPassword: appPassword.trim() }); - setStatus({ ...status, hasAppPassword: true }); + await client.put('/integrations/google/app-password', { + appPassword: appPassword.trim(), + email: gmailEmail.trim(), + }); + setStatus({ ...status, hasAppPassword: true, email: gmailEmail.trim() }); setAppPassword(''); setShowPasswordInput(false); toast.success('App password saved'); @@ -130,6 +151,9 @@ export const GoogleAccount = () => { {status.hasAppPassword && !showPasswordInput ? (
Configured + {status.email && ( + ({status.email}) + )}
-
+
setAppPassword(ev.target.value)} - placeholder="xxxx xxxx xxxx xxxx" - className="flex-1 h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" + type="email" + value={gmailEmail} + onChange={(ev) => setGmailEmail(ev.target.value)} + placeholder="your@gmail.com" + className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" /> - +
+ setAppPassword(ev.target.value)} + placeholder="xxxx xxxx xxxx xxxx" + className="flex-1 h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" + /> + +
)} @@ -250,7 +283,12 @@ export const GoogleAccount = () => {

{status.email}

{status.picture && ( - + )}