@realtimex/email-automator 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +4 -8
- package/api/src/config/index.ts +6 -3
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +7 -11
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +88 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -5
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
package/src/App.tsx
DELETED
|
@@ -1,622 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { Mail, LayoutDashboard, Settings, BarChart3, LogOut, Clock, Cpu, Brain, Zap, AlertCircle, Info, Code, CheckCircle2, UserCircle } from 'lucide-react';
|
|
3
|
-
import { ThemeProvider } from './components/theme-provider';
|
|
4
|
-
import { ModeToggle } from './components/mode-toggle';
|
|
5
|
-
import { Button } from './components/ui/button';
|
|
6
|
-
import { AppProvider, useApp } from './context/AppContext';
|
|
7
|
-
import { MigrationProvider } from './context/MigrationContext';
|
|
8
|
-
import { TerminalProvider } from './context/TerminalContext';
|
|
9
|
-
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
10
|
-
import { ToastContainer, toast } from './components/Toast';
|
|
11
|
-
import { PageLoader } from './components/LoadingSpinner';
|
|
12
|
-
import { SetupWizard } from './components/SetupWizard';
|
|
13
|
-
import { Dashboard } from './components/Dashboard';
|
|
14
|
-
import { Configuration } from "./components/Configuration";
|
|
15
|
-
import { AccountSettingsPage } from './components/AccountSettingsPage';
|
|
16
|
-
import { Login } from './components/Login';
|
|
17
|
-
import { Logo } from './components/Logo';
|
|
18
|
-
import { getSupabaseConfig, validateSupabaseConnection } from './lib/supabase-config';
|
|
19
|
-
import { supabase } from './lib/supabase';
|
|
20
|
-
import { api } from './lib/api';
|
|
21
|
-
import { cn } from './lib/utils';
|
|
22
|
-
import {
|
|
23
|
-
checkMigrationStatus,
|
|
24
|
-
type MigrationStatus,
|
|
25
|
-
isMigrationReminderDismissed
|
|
26
|
-
} from './lib/migration-check';
|
|
27
|
-
import { MigrationBanner } from './components/migration/MigrationBanner';
|
|
28
|
-
import { MigrationModal } from './components/migration/MigrationModal';
|
|
29
|
-
import { LiveTerminal } from './components/LiveTerminal';
|
|
30
|
-
import { ProcessingEvent } from './lib/types';
|
|
31
|
-
import {
|
|
32
|
-
Dialog,
|
|
33
|
-
DialogContent,
|
|
34
|
-
DialogDescription,
|
|
35
|
-
DialogHeader,
|
|
36
|
-
DialogTitle,
|
|
37
|
-
} from './components/ui/dialog';
|
|
38
|
-
|
|
39
|
-
type TabType = 'dashboard' | 'config' | 'analytics' | 'account';
|
|
40
|
-
|
|
41
|
-
function AppContent() {
|
|
42
|
-
const { state, actions } = useApp();
|
|
43
|
-
const [needsSetup, setNeedsSetup] = useState(false);
|
|
44
|
-
const [activeTab, setActiveTab] = useState<TabType>('dashboard');
|
|
45
|
-
const [checkingConfig, setCheckingConfig] = useState(true);
|
|
46
|
-
const [processingAuth, setProcessingAuth] = useState(false);
|
|
47
|
-
|
|
48
|
-
// Handle OAuth Callback (e.g. Gmail)
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const params = new URLSearchParams(window.location.search);
|
|
51
|
-
const code = params.get('code');
|
|
52
|
-
|
|
53
|
-
if (code && !processingAuth) {
|
|
54
|
-
const handleCallback = async () => {
|
|
55
|
-
setProcessingAuth(true);
|
|
56
|
-
try {
|
|
57
|
-
// Try Gmail connection
|
|
58
|
-
// Note: In a robust app, we should pass 'state' param to know which provider
|
|
59
|
-
// but since MS uses device flow here, it's likely Gmail.
|
|
60
|
-
const response = await api.connectGmail(code);
|
|
61
|
-
if (response.data?.success) {
|
|
62
|
-
toast.success('Gmail connected successfully!');
|
|
63
|
-
// Notify opener if exists
|
|
64
|
-
if (window.opener) {
|
|
65
|
-
// Close popup after short delay
|
|
66
|
-
setTimeout(() => window.close(), 1500);
|
|
67
|
-
} else {
|
|
68
|
-
// Clear URL
|
|
69
|
-
window.history.replaceState({}, '', window.location.pathname);
|
|
70
|
-
actions.fetchAccounts();
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
const errMsg = typeof response.error === 'string'
|
|
74
|
-
? response.error
|
|
75
|
-
: response.error?.message;
|
|
76
|
-
toast.error(errMsg || 'Failed to connect Gmail');
|
|
77
|
-
}
|
|
78
|
-
} catch (error) {
|
|
79
|
-
toast.error('Connection failed');
|
|
80
|
-
} finally {
|
|
81
|
-
setProcessingAuth(false);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
handleCallback();
|
|
85
|
-
}
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
if (processingAuth) {
|
|
89
|
-
return <PageLoader text="Connecting account..." />;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Migration state
|
|
93
|
-
const [migrationStatus, setMigrationStatus] = useState<MigrationStatus | null>(null);
|
|
94
|
-
const [showMigrationBanner, setShowMigrationBanner] = useState(false);
|
|
95
|
-
const [showMigrationModal, setShowMigrationModal] = useState(false);
|
|
96
|
-
const [suppressMigrationBanner, setSuppressMigrationBanner] = useState(false);
|
|
97
|
-
|
|
98
|
-
// Initial Config Check
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
const checkConfig = async () => {
|
|
101
|
-
const config = getSupabaseConfig();
|
|
102
|
-
|
|
103
|
-
if (!config) {
|
|
104
|
-
setNeedsSetup(true);
|
|
105
|
-
setCheckingConfig(false);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Validate the configuration (especially if it came from environment variables)
|
|
110
|
-
const validation = await validateSupabaseConnection(config.url, config.anonKey);
|
|
111
|
-
|
|
112
|
-
if (!validation.valid) {
|
|
113
|
-
// Force setup wizard on invalid config
|
|
114
|
-
setNeedsSetup(true);
|
|
115
|
-
setCheckingConfig(false);
|
|
116
|
-
return;
|
|
117
|
-
} else if (state.isInitialized && state.isAuthenticated) {
|
|
118
|
-
// Load initial data only after initialization and auth
|
|
119
|
-
actions.fetchAccounts();
|
|
120
|
-
actions.fetchRules();
|
|
121
|
-
actions.fetchSettings();
|
|
122
|
-
actions.fetchProfile();
|
|
123
|
-
|
|
124
|
-
// Check migration status
|
|
125
|
-
checkMigrationStatus(supabase).then((status) => {
|
|
126
|
-
setMigrationStatus(status);
|
|
127
|
-
if (status.needsMigration && !isMigrationReminderDismissed()) {
|
|
128
|
-
setShowMigrationBanner(true);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
setCheckingConfig(false);
|
|
133
|
-
};
|
|
134
|
-
checkConfig();
|
|
135
|
-
}, [state.isInitialized, state.isAuthenticated]);
|
|
136
|
-
|
|
137
|
-
const handleOpenMigrationModal = () => {
|
|
138
|
-
setShowMigrationModal(true);
|
|
139
|
-
setShowMigrationBanner(false);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const migrationContextValue = {
|
|
143
|
-
migrationStatus,
|
|
144
|
-
showMigrationBanner,
|
|
145
|
-
showMigrationModal,
|
|
146
|
-
openMigrationModal: handleOpenMigrationModal,
|
|
147
|
-
suppressMigrationBanner,
|
|
148
|
-
setSuppressMigrationBanner,
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
if (checkingConfig) {
|
|
152
|
-
return <PageLoader text="Checking configuration..." />;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (needsSetup) {
|
|
156
|
-
return (
|
|
157
|
-
<SetupWizard onComplete={() => {
|
|
158
|
-
setNeedsSetup(false);
|
|
159
|
-
window.location.reload();
|
|
160
|
-
}} />
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!state.isInitialized) {
|
|
165
|
-
return <PageLoader text="Initializing..." />;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Show login if not authenticated
|
|
169
|
-
if (!state.isAuthenticated) {
|
|
170
|
-
return <Login onConfigure={() => setNeedsSetup(true)} />;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const handleLogout = async () => {
|
|
174
|
-
await supabase.auth.signOut();
|
|
175
|
-
toast.success('Logged out successfully');
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<MigrationProvider value={migrationContextValue}>
|
|
180
|
-
<div className="min-h-screen bg-background font-sans text-foreground transition-colors duration-300">
|
|
181
|
-
{/* Header */}
|
|
182
|
-
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
183
|
-
<div className="max-w-7xl mx-auto flex h-16 items-center justify-between px-4 sm:px-8">
|
|
184
|
-
<div className="flex items-center gap-4">
|
|
185
|
-
<button
|
|
186
|
-
onClick={() => setActiveTab('dashboard')}
|
|
187
|
-
className="text-xl font-bold flex items-center gap-2 hover:opacity-80 transition-opacity"
|
|
188
|
-
>
|
|
189
|
-
<Logo className="w-9 h-9" />
|
|
190
|
-
<span className="hidden sm:inline">Email Automator</span>
|
|
191
|
-
<span className="sm:hidden">Email AI</span>
|
|
192
|
-
</button>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<div className="flex gap-4 items-center">
|
|
196
|
-
<nav className="flex items-center gap-1 bg-secondary/50 p-1 rounded-lg">
|
|
197
|
-
<Button
|
|
198
|
-
variant={activeTab === 'dashboard' ? 'secondary' : 'ghost'}
|
|
199
|
-
size="sm"
|
|
200
|
-
onClick={() => setActiveTab('dashboard')}
|
|
201
|
-
className="gap-2"
|
|
202
|
-
>
|
|
203
|
-
<LayoutDashboard className="w-4 h-4" />
|
|
204
|
-
<span className="hidden sm:inline">Dashboard</span>
|
|
205
|
-
</Button>
|
|
206
|
-
<Button
|
|
207
|
-
variant={activeTab === 'analytics' ? 'secondary' : 'ghost'}
|
|
208
|
-
size="sm"
|
|
209
|
-
onClick={() => setActiveTab('analytics')}
|
|
210
|
-
className="gap-2"
|
|
211
|
-
>
|
|
212
|
-
<BarChart3 className="w-4 h-4" />
|
|
213
|
-
<span className="hidden sm:inline">Analytics</span>
|
|
214
|
-
</Button>
|
|
215
|
-
<Button
|
|
216
|
-
variant={activeTab === 'config' ? 'secondary' : 'ghost'}
|
|
217
|
-
size="sm"
|
|
218
|
-
onClick={() => setActiveTab('config')}
|
|
219
|
-
className="gap-2"
|
|
220
|
-
>
|
|
221
|
-
<Settings className="w-4 h-4" />
|
|
222
|
-
<span className="hidden sm:inline">Configuration</span>
|
|
223
|
-
</Button>
|
|
224
|
-
</nav>
|
|
225
|
-
<div className="h-6 w-px bg-border/50 mx-2 hidden sm:block" />
|
|
226
|
-
<ModeToggle />
|
|
227
|
-
<Button
|
|
228
|
-
variant={activeTab === 'account' ? 'secondary' : 'ghost'}
|
|
229
|
-
size="sm"
|
|
230
|
-
onClick={() => setActiveTab('account')}
|
|
231
|
-
className="text-muted-foreground hover:text-foreground p-0 w-8 h-8 rounded-full overflow-hidden border"
|
|
232
|
-
title="Account Settings"
|
|
233
|
-
>
|
|
234
|
-
{state.profile?.avatar_url ? (
|
|
235
|
-
<img src={state.profile.avatar_url} alt="Profile" className="w-full h-full object-cover" />
|
|
236
|
-
) : (
|
|
237
|
-
<UserCircle className="w-5 h-5" />
|
|
238
|
-
)}
|
|
239
|
-
</Button>
|
|
240
|
-
<Button
|
|
241
|
-
variant="ghost"
|
|
242
|
-
size="sm"
|
|
243
|
-
onClick={handleLogout}
|
|
244
|
-
className="text-muted-foreground hover:text-foreground"
|
|
245
|
-
title="Sign out"
|
|
246
|
-
>
|
|
247
|
-
<LogOut className="w-4 h-4" />
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
</header>
|
|
252
|
-
|
|
253
|
-
{/* Main Content */}
|
|
254
|
-
<main className="max-w-7xl mx-auto p-4 sm:p-8 mt-4">
|
|
255
|
-
{activeTab === 'dashboard' && <Dashboard />}
|
|
256
|
-
{activeTab === 'config' && <Configuration />}
|
|
257
|
-
{activeTab === 'analytics' && <AnalyticsPage />}
|
|
258
|
-
{activeTab === 'account' && <AccountSettingsPage />}
|
|
259
|
-
</main>
|
|
260
|
-
|
|
261
|
-
{/* Error Display */}
|
|
262
|
-
{state.error && (
|
|
263
|
-
<div className="fixed bottom-20 left-4 right-4 sm:left-auto sm:right-4 sm:max-w-sm z-50">
|
|
264
|
-
<div className="bg-destructive/10 border border-destructive/20 text-destructive p-4 rounded-lg">
|
|
265
|
-
<p className="text-sm">{state.error}</p>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
269
|
-
|
|
270
|
-
{/* Migration UI */}
|
|
271
|
-
{migrationStatus && showMigrationBanner && !suppressMigrationBanner && (
|
|
272
|
-
<MigrationBanner
|
|
273
|
-
status={migrationStatus}
|
|
274
|
-
onDismiss={() => setShowMigrationBanner(false)}
|
|
275
|
-
onLearnMore={handleOpenMigrationModal}
|
|
276
|
-
/>
|
|
277
|
-
)}
|
|
278
|
-
|
|
279
|
-
{migrationStatus && (
|
|
280
|
-
<MigrationModal
|
|
281
|
-
open={showMigrationModal}
|
|
282
|
-
onOpenChange={setShowMigrationModal}
|
|
283
|
-
status={migrationStatus}
|
|
284
|
-
/>
|
|
285
|
-
)}
|
|
286
|
-
|
|
287
|
-
<LiveTerminal />
|
|
288
|
-
</div>
|
|
289
|
-
</MigrationProvider>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function RunTraceModal({
|
|
294
|
-
runId,
|
|
295
|
-
accountEmail,
|
|
296
|
-
isOpen,
|
|
297
|
-
onOpenChange
|
|
298
|
-
}: {
|
|
299
|
-
runId: string | null,
|
|
300
|
-
accountEmail?: string,
|
|
301
|
-
isOpen: boolean,
|
|
302
|
-
onOpenChange: (open: boolean) => void
|
|
303
|
-
}) {
|
|
304
|
-
const [events, setEvents] = useState<ProcessingEvent[]>([]);
|
|
305
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
306
|
-
|
|
307
|
-
useEffect(() => {
|
|
308
|
-
if (isOpen && runId) {
|
|
309
|
-
fetchEvents();
|
|
310
|
-
}
|
|
311
|
-
}, [isOpen, runId]);
|
|
312
|
-
|
|
313
|
-
const fetchEvents = async () => {
|
|
314
|
-
if (!runId) return;
|
|
315
|
-
setIsLoading(true);
|
|
316
|
-
try {
|
|
317
|
-
const response = await api.getRunEvents(runId);
|
|
318
|
-
if (response.data) {
|
|
319
|
-
setEvents(response.data.events);
|
|
320
|
-
}
|
|
321
|
-
} catch (error) {
|
|
322
|
-
console.error('Failed to fetch run trace:', error);
|
|
323
|
-
} finally {
|
|
324
|
-
setIsLoading(false);
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const getIcon = (type: string) => {
|
|
329
|
-
switch (type) {
|
|
330
|
-
case 'analysis': return <Brain className="w-4 h-4 text-purple-500" />;
|
|
331
|
-
case 'action': return <Zap className="w-4 h-4 text-emerald-500" />;
|
|
332
|
-
case 'error': return <AlertCircle className="w-4 h-4 text-red-500" />;
|
|
333
|
-
default: return <Info className="w-4 h-4 text-blue-500" />;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
339
|
-
<DialogContent className="sm:max-w-2xl max-h-[85vh] flex flex-col p-0 overflow-hidden">
|
|
340
|
-
<DialogHeader className="p-6 border-b">
|
|
341
|
-
<div className="flex items-center gap-2">
|
|
342
|
-
<Cpu className="w-5 h-5 text-primary" />
|
|
343
|
-
<DialogTitle>Sync Run Trace</DialogTitle>
|
|
344
|
-
</div>
|
|
345
|
-
<DialogDescription>
|
|
346
|
-
{accountEmail ? `Full log for account: ${accountEmail}` : 'Historical log for this synchronization run.'}
|
|
347
|
-
</DialogDescription>
|
|
348
|
-
</DialogHeader>
|
|
349
|
-
|
|
350
|
-
<div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar bg-secondary/5">
|
|
351
|
-
{isLoading ? (
|
|
352
|
-
<div className="py-20 flex justify-center"><PageLoader text="Loading trace..." /></div>
|
|
353
|
-
) : events.length === 0 ? (
|
|
354
|
-
<div className="py-20 text-center text-muted-foreground italic font-mono text-sm">
|
|
355
|
-
No granular trace events found for this run.
|
|
356
|
-
</div>
|
|
357
|
-
) : (
|
|
358
|
-
events.map((event, i) => (
|
|
359
|
-
<div key={event.id} className="relative pl-8">
|
|
360
|
-
{/* Timeline Line */}
|
|
361
|
-
{i !== events.length - 1 && (
|
|
362
|
-
<div className="absolute left-[15px] top-8 bottom-[-24px] w-px bg-border" />
|
|
363
|
-
)}
|
|
364
|
-
|
|
365
|
-
{/* Icon Badge */}
|
|
366
|
-
<div className="absolute left-0 top-0 w-8 h-8 rounded-full border bg-background flex items-center justify-center z-10 shadow-sm">
|
|
367
|
-
{getIcon(event.event_type)}
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
<div className="space-y-2">
|
|
371
|
-
<div className="flex items-center justify-between">
|
|
372
|
-
<div className="flex flex-col">
|
|
373
|
-
<span className="text-[10px] font-bold uppercase tracking-wider text-foreground/70">
|
|
374
|
-
{event.agent_state}
|
|
375
|
-
</span>
|
|
376
|
-
{(event as any).emails?.subject && (
|
|
377
|
-
<span className="text-[10px] text-primary font-medium truncate max-w-[300px]">
|
|
378
|
-
Re: {(event as any).emails.subject}
|
|
379
|
-
</span>
|
|
380
|
-
)}
|
|
381
|
-
</div>
|
|
382
|
-
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
|
383
|
-
<Clock className="w-3 h-3" />
|
|
384
|
-
{new Date(event.created_at).toLocaleTimeString()}
|
|
385
|
-
</span>
|
|
386
|
-
</div>
|
|
387
|
-
|
|
388
|
-
{/* Event Details */}
|
|
389
|
-
<div className="bg-card border rounded-lg p-4 shadow-sm">
|
|
390
|
-
{event.event_type === 'info' && (
|
|
391
|
-
<p className="text-sm text-foreground/90">{event.details?.message}</p>
|
|
392
|
-
)}
|
|
393
|
-
|
|
394
|
-
{event.event_type === 'analysis' && (
|
|
395
|
-
<div className="space-y-2">
|
|
396
|
-
<p className="text-xs text-foreground italic leading-relaxed">
|
|
397
|
-
"{event.details?.summary}"
|
|
398
|
-
</p>
|
|
399
|
-
<div className="flex gap-2">
|
|
400
|
-
<span className="text-[9px] bg-secondary px-1.5 py-0.5 rounded font-bold uppercase">
|
|
401
|
-
{event.details?.category}
|
|
402
|
-
</span>
|
|
403
|
-
{event.details?.suggested_actions?.map((a: string) => (
|
|
404
|
-
<span key={a} className="text-[9px] bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-1.5 py-0.5 rounded border border-emerald-500/20 font-bold uppercase">
|
|
405
|
-
{a}
|
|
406
|
-
</span>
|
|
407
|
-
))}
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
)}
|
|
411
|
-
|
|
412
|
-
{event.event_type === 'action' && (
|
|
413
|
-
<div className="flex items-center justify-between">
|
|
414
|
-
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 capitalize">
|
|
415
|
-
Executed: {event.details?.action}
|
|
416
|
-
</p>
|
|
417
|
-
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
|
418
|
-
</div>
|
|
419
|
-
)}
|
|
420
|
-
|
|
421
|
-
{event.event_type === 'error' && (
|
|
422
|
-
<p className="text-sm text-red-600 dark:text-red-400 font-bold">
|
|
423
|
-
{event.details?.error}
|
|
424
|
-
</p>
|
|
425
|
-
)}
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
))
|
|
430
|
-
)}
|
|
431
|
-
</div>
|
|
432
|
-
</DialogContent>
|
|
433
|
-
</Dialog>
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function AnalyticsPage() {
|
|
438
|
-
const { state, actions } = useApp();
|
|
439
|
-
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
|
|
440
|
-
const [selectedAccountEmail, setSelectedAccountEmail] = useState<string | undefined>(undefined);
|
|
441
|
-
const [isRunTraceOpen, setIsRunTraceOpen] = useState(false);
|
|
442
|
-
|
|
443
|
-
useEffect(() => {
|
|
444
|
-
actions.fetchStats();
|
|
445
|
-
}, []);
|
|
446
|
-
|
|
447
|
-
if (!state.stats) {
|
|
448
|
-
return <PageLoader text="Loading analytics..." />;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const handleViewRunTrace = (runId: string, email?: string) => {
|
|
452
|
-
setSelectedRunId(runId);
|
|
453
|
-
setSelectedAccountEmail(email);
|
|
454
|
-
setIsRunTraceOpen(true);
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const { stats } = state;
|
|
458
|
-
|
|
459
|
-
return (
|
|
460
|
-
<div className="space-y-8 animate-in fade-in duration-500">
|
|
461
|
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
|
462
|
-
<BarChart3 className="w-6 h-6 text-primary" />
|
|
463
|
-
Analytics Dashboard
|
|
464
|
-
</h2>
|
|
465
|
-
|
|
466
|
-
{/* Summary Cards */}
|
|
467
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
468
|
-
<StatCard
|
|
469
|
-
title="Total Emails"
|
|
470
|
-
value={stats.totalEmails}
|
|
471
|
-
color="primary"
|
|
472
|
-
/>
|
|
473
|
-
<StatCard
|
|
474
|
-
title="Spam Caught"
|
|
475
|
-
value={stats.categoryCounts['spam'] || 0}
|
|
476
|
-
color="destructive"
|
|
477
|
-
/>
|
|
478
|
-
<StatCard
|
|
479
|
-
title="Actions Taken"
|
|
480
|
-
value={Object.values(stats.actionCounts).reduce((a, b) => a + b, 0) - (stats.actionCounts['none'] || 0)}
|
|
481
|
-
color="emerald"
|
|
482
|
-
/>
|
|
483
|
-
<StatCard
|
|
484
|
-
title="Accounts"
|
|
485
|
-
value={stats.accountCount}
|
|
486
|
-
color="blue"
|
|
487
|
-
/>
|
|
488
|
-
</div>
|
|
489
|
-
|
|
490
|
-
{/* Category Breakdown */}
|
|
491
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
492
|
-
<div className="bg-card border rounded-xl p-6">
|
|
493
|
-
<h3 className="font-semibold mb-4">Email Categories</h3>
|
|
494
|
-
<div className="space-y-3">
|
|
495
|
-
{Object.entries(stats.categoryCounts).map(([category, count]) => (
|
|
496
|
-
<div key={category} className="flex items-center gap-3">
|
|
497
|
-
<div className="flex-1">
|
|
498
|
-
<div className="flex justify-between text-sm mb-1">
|
|
499
|
-
<span className="capitalize">{category}</span>
|
|
500
|
-
<span className="text-muted-foreground">{count}</span>
|
|
501
|
-
</div>
|
|
502
|
-
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
503
|
-
<div
|
|
504
|
-
className="h-full bg-primary rounded-full transition-all"
|
|
505
|
-
style={{ width: `${(count / stats.totalEmails) * 100}%` }}
|
|
506
|
-
/>
|
|
507
|
-
</div>
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
510
|
-
))}
|
|
511
|
-
</div>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
<div className="bg-card border rounded-xl p-6">
|
|
515
|
-
<h3 className="font-semibold mb-4">Actions Taken</h3>
|
|
516
|
-
<div className="space-y-3">
|
|
517
|
-
{Object.entries(stats.actionCounts).map(([action, count]) => (
|
|
518
|
-
<div key={action} className="flex items-center justify-between py-2 border-b last:border-0">
|
|
519
|
-
<span className="capitalize">{action}</span>
|
|
520
|
-
<span className="font-medium">{count}</span>
|
|
521
|
-
</div>
|
|
522
|
-
))}
|
|
523
|
-
</div>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
526
|
-
|
|
527
|
-
{/* Recent Syncs */}
|
|
528
|
-
<div className="bg-card border rounded-xl p-6">
|
|
529
|
-
<h3 className="font-semibold mb-4">Recent Sync Activity</h3>
|
|
530
|
-
{stats.recentSyncs.length === 0 ? (
|
|
531
|
-
<p className="text-muted-foreground text-sm">No sync activity yet</p>
|
|
532
|
-
) : (
|
|
533
|
-
<div className="space-y-3">
|
|
534
|
-
{stats.recentSyncs.map((log: any) => {
|
|
535
|
-
const duration = log.completed_at
|
|
536
|
-
? Math.round((new Date(log.completed_at).getTime() - new Date(log.started_at).getTime()) / 1000)
|
|
537
|
-
: null;
|
|
538
|
-
|
|
539
|
-
return (
|
|
540
|
-
<div
|
|
541
|
-
key={log.id}
|
|
542
|
-
className="flex flex-col sm:flex-row sm:items-center justify-between p-3 border rounded-lg hover:bg-secondary/30 transition-colors gap-3 cursor-pointer group"
|
|
543
|
-
onClick={() => handleViewRunTrace(log.id, log.email_accounts?.email_address)}
|
|
544
|
-
>
|
|
545
|
-
<div className="flex items-center gap-3">
|
|
546
|
-
<div className={cn(
|
|
547
|
-
"w-2.5 h-2.5 rounded-full",
|
|
548
|
-
log.status === 'success' ? 'bg-emerald-500' :
|
|
549
|
-
log.status === 'failed' ? 'bg-destructive' : 'bg-yellow-500 shadow-[0_0_8px_rgba(234,179,8,0.5)] animate-pulse'
|
|
550
|
-
)} />
|
|
551
|
-
<div className="flex flex-col">
|
|
552
|
-
<span className="text-sm font-medium group-hover:text-primary transition-colors">
|
|
553
|
-
{log.email_accounts?.email_address || 'System Sync'}
|
|
554
|
-
</span>
|
|
555
|
-
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
|
556
|
-
<Clock className="w-3 h-3" />
|
|
557
|
-
{new Date(log.started_at).toLocaleString()}
|
|
558
|
-
{duration !== null && (
|
|
559
|
-
<span className="ml-2 px-1.5 py-0.5 bg-secondary rounded-full">
|
|
560
|
-
{duration}s
|
|
561
|
-
</span>
|
|
562
|
-
)}
|
|
563
|
-
</span>
|
|
564
|
-
</div>
|
|
565
|
-
</div>
|
|
566
|
-
<div className="flex items-center gap-4 text-xs">
|
|
567
|
-
<div className="flex flex-col items-end">
|
|
568
|
-
<span className="font-bold text-primary">{log.emails_processed} emails</span>
|
|
569
|
-
<span className="text-[10px] text-muted-foreground">
|
|
570
|
-
{log.emails_deleted} deleted, {log.emails_drafted} drafted
|
|
571
|
-
</span>
|
|
572
|
-
</div>
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
);
|
|
576
|
-
})}
|
|
577
|
-
</div>
|
|
578
|
-
)}
|
|
579
|
-
</div>
|
|
580
|
-
|
|
581
|
-
<RunTraceModal
|
|
582
|
-
runId={selectedRunId}
|
|
583
|
-
accountEmail={selectedAccountEmail}
|
|
584
|
-
isOpen={isRunTraceOpen}
|
|
585
|
-
onOpenChange={setIsRunTraceOpen}
|
|
586
|
-
/>
|
|
587
|
-
</div>
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function StatCard({ title, value, color }: { title: string; value: number; color: string }) {
|
|
592
|
-
const colorClasses: Record<string, string> = {
|
|
593
|
-
primary: 'bg-primary/10 text-primary',
|
|
594
|
-
destructive: 'bg-destructive/10 text-destructive',
|
|
595
|
-
emerald: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
|
596
|
-
blue: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
return (
|
|
600
|
-
<div className="bg-card border rounded-xl p-6">
|
|
601
|
-
<p className="text-sm text-muted-foreground mb-1">{title}</p>
|
|
602
|
-
<p className={`text-3xl font-bold ${colorClasses[color] || ''}`}>{value}</p>
|
|
603
|
-
</div>
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function App() {
|
|
608
|
-
return (
|
|
609
|
-
<ThemeProvider defaultTheme="system" storageKey="email-automator-theme">
|
|
610
|
-
<ErrorBoundary>
|
|
611
|
-
<TerminalProvider>
|
|
612
|
-
<AppProvider>
|
|
613
|
-
<AppContent />
|
|
614
|
-
<ToastContainer />
|
|
615
|
-
</AppProvider>
|
|
616
|
-
</TerminalProvider>
|
|
617
|
-
</ErrorBoundary>
|
|
618
|
-
</ThemeProvider>
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
export default App;
|