@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.
Files changed (74) hide show
  1. package/api/server.ts +4 -8
  2. package/api/src/config/index.ts +6 -3
  3. package/bin/email-automator-setup.js +2 -3
  4. package/bin/email-automator.js +7 -11
  5. package/dist/api/server.js +109 -0
  6. package/dist/api/src/config/index.js +88 -0
  7. package/dist/api/src/middleware/auth.js +119 -0
  8. package/dist/api/src/middleware/errorHandler.js +78 -0
  9. package/dist/api/src/middleware/index.js +4 -0
  10. package/dist/api/src/middleware/rateLimit.js +57 -0
  11. package/dist/api/src/middleware/validation.js +111 -0
  12. package/dist/api/src/routes/actions.js +173 -0
  13. package/dist/api/src/routes/auth.js +106 -0
  14. package/dist/api/src/routes/emails.js +100 -0
  15. package/dist/api/src/routes/health.js +33 -0
  16. package/dist/api/src/routes/index.js +19 -0
  17. package/dist/api/src/routes/migrate.js +61 -0
  18. package/dist/api/src/routes/rules.js +104 -0
  19. package/dist/api/src/routes/settings.js +178 -0
  20. package/dist/api/src/routes/sync.js +118 -0
  21. package/dist/api/src/services/eventLogger.js +41 -0
  22. package/dist/api/src/services/gmail.js +350 -0
  23. package/dist/api/src/services/intelligence.js +243 -0
  24. package/dist/api/src/services/microsoft.js +256 -0
  25. package/dist/api/src/services/processor.js +503 -0
  26. package/dist/api/src/services/scheduler.js +210 -0
  27. package/dist/api/src/services/supabase.js +59 -0
  28. package/dist/api/src/utils/contentCleaner.js +94 -0
  29. package/dist/api/src/utils/crypto.js +68 -0
  30. package/dist/api/src/utils/logger.js +119 -0
  31. package/package.json +5 -5
  32. package/src/App.tsx +0 -622
  33. package/src/components/AccountSettings.tsx +0 -310
  34. package/src/components/AccountSettingsPage.tsx +0 -390
  35. package/src/components/Configuration.tsx +0 -1345
  36. package/src/components/Dashboard.tsx +0 -940
  37. package/src/components/ErrorBoundary.tsx +0 -71
  38. package/src/components/LiveTerminal.tsx +0 -308
  39. package/src/components/LoadingSpinner.tsx +0 -39
  40. package/src/components/Login.tsx +0 -371
  41. package/src/components/Logo.tsx +0 -57
  42. package/src/components/SetupWizard.tsx +0 -388
  43. package/src/components/Toast.tsx +0 -109
  44. package/src/components/migration/MigrationBanner.tsx +0 -97
  45. package/src/components/migration/MigrationModal.tsx +0 -458
  46. package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
  47. package/src/components/mode-toggle.tsx +0 -24
  48. package/src/components/theme-provider.tsx +0 -72
  49. package/src/components/ui/alert.tsx +0 -66
  50. package/src/components/ui/button.tsx +0 -57
  51. package/src/components/ui/card.tsx +0 -75
  52. package/src/components/ui/dialog.tsx +0 -133
  53. package/src/components/ui/input.tsx +0 -22
  54. package/src/components/ui/label.tsx +0 -24
  55. package/src/components/ui/otp-input.tsx +0 -184
  56. package/src/context/AppContext.tsx +0 -422
  57. package/src/context/MigrationContext.tsx +0 -53
  58. package/src/context/TerminalContext.tsx +0 -31
  59. package/src/core/actions.ts +0 -76
  60. package/src/core/auth.ts +0 -108
  61. package/src/core/intelligence.ts +0 -76
  62. package/src/core/processor.ts +0 -112
  63. package/src/hooks/useRealtimeEmails.ts +0 -111
  64. package/src/index.css +0 -140
  65. package/src/lib/api-config.ts +0 -42
  66. package/src/lib/api-old.ts +0 -228
  67. package/src/lib/api.ts +0 -421
  68. package/src/lib/migration-check.ts +0 -264
  69. package/src/lib/sounds.ts +0 -120
  70. package/src/lib/supabase-config.ts +0 -117
  71. package/src/lib/supabase.ts +0 -28
  72. package/src/lib/types.ts +0 -166
  73. package/src/lib/utils.ts +0 -6
  74. 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;