@realtimex/email-automator 2.1.1 → 2.2.1
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 +0 -6
- package/api/src/config/index.ts +3 -0
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +23 -7
- package/package.json +1 -2
- 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
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration Version Check Utility
|
|
3
|
-
*
|
|
4
|
-
* This module provides functions to detect if the database needs migration
|
|
5
|
-
* by comparing the app version with the database schema version.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get the current app version from package.json
|
|
12
|
-
*/
|
|
13
|
-
export const APP_VERSION = import.meta.env.VITE_APP_VERSION;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get the latest migration timestamp bundled with this app
|
|
17
|
-
* Format: YYYYMMDDHHMMSS (e.g., "20251229213735")
|
|
18
|
-
*/
|
|
19
|
-
export const LATEST_MIGRATION_TIMESTAMP =
|
|
20
|
-
import.meta.env.VITE_LATEST_MIGRATION_TIMESTAMP;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Compare two semantic versions (e.g., "0.31.0" vs "0.30.0")
|
|
24
|
-
* Returns:
|
|
25
|
-
* 1 if v1 > v2
|
|
26
|
-
* 0 if v1 === v2
|
|
27
|
-
* -1 if v1 < v2
|
|
28
|
-
*/
|
|
29
|
-
export function compareSemver(v1: string, v2: string): number {
|
|
30
|
-
const parts1 = v1.split('.').map(Number);
|
|
31
|
-
const parts2 = v2.split('.').map(Number);
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
34
|
-
const num1 = parts1[i] || 0;
|
|
35
|
-
const num2 = parts2[i] || 0;
|
|
36
|
-
|
|
37
|
-
if (num1 > num2) return 1;
|
|
38
|
-
if (num1 < num2) return -1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Database migration info
|
|
46
|
-
*/
|
|
47
|
-
export interface DatabaseMigrationInfo {
|
|
48
|
-
version: string | null;
|
|
49
|
-
latestMigrationTimestamp: string | null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the latest applied migration info from the database
|
|
54
|
-
* Uses Supabase's internal migration tracking via a database function
|
|
55
|
-
*/
|
|
56
|
-
export async function getDatabaseMigrationInfo(
|
|
57
|
-
supabase: SupabaseClient,
|
|
58
|
-
): Promise<DatabaseMigrationInfo> {
|
|
59
|
-
try {
|
|
60
|
-
// Call database function that queries Supabase's internal migration table
|
|
61
|
-
// This is automatically updated by `supabase db push`
|
|
62
|
-
const { data, error } = await supabase
|
|
63
|
-
.rpc('get_latest_migration_timestamp');
|
|
64
|
-
|
|
65
|
-
if (error) {
|
|
66
|
-
console.warn('Could not get latest migration timestamp:', error.message);
|
|
67
|
-
return { version: null, latestMigrationTimestamp: null };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// The returned value IS the migration timestamp (e.g., "20251230082455")
|
|
71
|
-
const latestTimestamp = data || null;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
version: APP_VERSION, // Use app version for display
|
|
75
|
-
latestMigrationTimestamp: latestTimestamp,
|
|
76
|
-
};
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error('Error checking database migration info:', error);
|
|
79
|
-
return { version: null, latestMigrationTimestamp: null };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Migration status result
|
|
85
|
-
*/
|
|
86
|
-
export interface MigrationStatus {
|
|
87
|
-
/** Whether migration is needed */
|
|
88
|
-
needsMigration: boolean;
|
|
89
|
-
/** Current app version */
|
|
90
|
-
appVersion: string;
|
|
91
|
-
/** Database schema version (null if unknown) */
|
|
92
|
-
dbVersion: string | null;
|
|
93
|
-
/** Human-readable status message */
|
|
94
|
-
message: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Check if database migration is needed
|
|
99
|
-
*
|
|
100
|
-
* Uses timestamp comparison for accurate migration detection:
|
|
101
|
-
* - Compares app's latest migration timestamp with DB's latest migration timestamp
|
|
102
|
-
* - If app timestamp > DB timestamp → new migrations available
|
|
103
|
-
*
|
|
104
|
-
* Fallback to SemVer comparison if DB lacks timestamp tracking (legacy schemas).
|
|
105
|
-
*
|
|
106
|
-
* @param supabase - Supabase client instance
|
|
107
|
-
* @returns Promise<MigrationStatus>
|
|
108
|
-
*/
|
|
109
|
-
export async function checkMigrationStatus(
|
|
110
|
-
supabase: SupabaseClient,
|
|
111
|
-
): Promise<MigrationStatus> {
|
|
112
|
-
const appVersion = APP_VERSION;
|
|
113
|
-
const appMigrationTimestamp = LATEST_MIGRATION_TIMESTAMP;
|
|
114
|
-
const dbInfo = await getDatabaseMigrationInfo(supabase);
|
|
115
|
-
|
|
116
|
-
console.log('[Migration Check]', {
|
|
117
|
-
appVersion,
|
|
118
|
-
appMigrationTimestamp,
|
|
119
|
-
dbVersion: dbInfo.version,
|
|
120
|
-
dbMigrationTimestamp: dbInfo.latestMigrationTimestamp,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// 1. Critical failure to determine app state
|
|
124
|
-
if (appMigrationTimestamp === 'unknown') {
|
|
125
|
-
return {
|
|
126
|
-
needsMigration: true,
|
|
127
|
-
appVersion,
|
|
128
|
-
dbVersion: dbInfo.version,
|
|
129
|
-
message: `App migration info missing. Migration to v${appVersion} likely needed.`,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 2. Database has timestamp tracking (Modern)
|
|
134
|
-
// Check for valid timestamp (not empty/whitespace)
|
|
135
|
-
if (
|
|
136
|
-
dbInfo.latestMigrationTimestamp &&
|
|
137
|
-
dbInfo.latestMigrationTimestamp.trim() !== ''
|
|
138
|
-
) {
|
|
139
|
-
const appTimestamp = appMigrationTimestamp;
|
|
140
|
-
const dbTimestamp = dbInfo.latestMigrationTimestamp;
|
|
141
|
-
|
|
142
|
-
if (appTimestamp > dbTimestamp) {
|
|
143
|
-
return {
|
|
144
|
-
needsMigration: true,
|
|
145
|
-
appVersion,
|
|
146
|
-
dbVersion: dbInfo.version,
|
|
147
|
-
message: `New migrations available. Database is at ${dbTimestamp}, app has ${appTimestamp}.`,
|
|
148
|
-
};
|
|
149
|
-
} else if (appTimestamp < dbTimestamp) {
|
|
150
|
-
console.warn('[Migration Check] DB is ahead of app - possible downgrade');
|
|
151
|
-
return {
|
|
152
|
-
needsMigration: false,
|
|
153
|
-
appVersion,
|
|
154
|
-
dbVersion: dbInfo.version,
|
|
155
|
-
message: `Database (${dbTimestamp}) is ahead of app (${appTimestamp}).`,
|
|
156
|
-
};
|
|
157
|
-
} else {
|
|
158
|
-
console.log('[Migration Check] Timestamps match - database is up-to-date');
|
|
159
|
-
return {
|
|
160
|
-
needsMigration: false,
|
|
161
|
-
appVersion,
|
|
162
|
-
dbVersion: dbInfo.version,
|
|
163
|
-
message: `Database schema is up-to-date.`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 3. Database has version but NO timestamp (Legacy Schema)
|
|
169
|
-
// Fallback to SemVer comparison
|
|
170
|
-
if (dbInfo.version) {
|
|
171
|
-
console.log(
|
|
172
|
-
'[Migration Check] Legacy DB detected (no timestamp). Falling back to SemVer.',
|
|
173
|
-
);
|
|
174
|
-
const comparison = compareSemver(appVersion, dbInfo.version);
|
|
175
|
-
|
|
176
|
-
if (comparison > 0) {
|
|
177
|
-
// App version is newer - definitely needs migration
|
|
178
|
-
return {
|
|
179
|
-
needsMigration: true,
|
|
180
|
-
appVersion,
|
|
181
|
-
dbVersion: dbInfo.version,
|
|
182
|
-
message: `Database schema (v${dbInfo.version}) is outdated. Migration to v${appVersion} required.`,
|
|
183
|
-
};
|
|
184
|
-
} else if (comparison === 0) {
|
|
185
|
-
// Versions match BUT we can't verify migrations without timestamps
|
|
186
|
-
// Be pessimistic: force migration to upgrade to modern timestamp tracking
|
|
187
|
-
console.warn(
|
|
188
|
-
'[Migration Check] Legacy DB with matching version - forcing migration to add timestamp tracking',
|
|
189
|
-
);
|
|
190
|
-
return {
|
|
191
|
-
needsMigration: true,
|
|
192
|
-
appVersion,
|
|
193
|
-
dbVersion: dbInfo.version,
|
|
194
|
-
message: `Database lacks timestamp tracking. Please run migration to upgrade to modern schema (v${appVersion}).`,
|
|
195
|
-
};
|
|
196
|
-
} else {
|
|
197
|
-
// DB version is ahead of app version
|
|
198
|
-
return {
|
|
199
|
-
needsMigration: false,
|
|
200
|
-
appVersion,
|
|
201
|
-
dbVersion: dbInfo.version,
|
|
202
|
-
message: `Database version (v${dbInfo.version}) is ahead of app (v${appVersion}).`,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 4. No DB info at all (Fresh DB or Error)
|
|
208
|
-
console.log('[Migration Check] No DB info found - assuming migration needed');
|
|
209
|
-
return {
|
|
210
|
-
needsMigration: true,
|
|
211
|
-
appVersion,
|
|
212
|
-
dbVersion: null,
|
|
213
|
-
message: `Database schema unknown. Migration required to v${appVersion}.`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* LocalStorage key for migration reminder dismissal
|
|
219
|
-
*/
|
|
220
|
-
const MIGRATION_REMINDER_KEY = 'email_automator_migration_reminder_dismissed_at';
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Check if user has dismissed the migration reminder recently
|
|
224
|
-
*
|
|
225
|
-
* @param hoursToWait - Hours to wait before showing reminder again (default: 24)
|
|
226
|
-
* @returns true if reminder was dismissed within the time window
|
|
227
|
-
*/
|
|
228
|
-
export function isMigrationReminderDismissed(hoursToWait = 24): boolean {
|
|
229
|
-
try {
|
|
230
|
-
const dismissedAt = localStorage.getItem(MIGRATION_REMINDER_KEY);
|
|
231
|
-
if (!dismissedAt) return false;
|
|
232
|
-
|
|
233
|
-
const dismissedTime = new Date(dismissedAt).getTime();
|
|
234
|
-
const now = Date.now();
|
|
235
|
-
const hoursSinceDismissal = (now - dismissedTime) / (1000 * 60 * 60);
|
|
236
|
-
|
|
237
|
-
return hoursSinceDismissal < hoursToWait;
|
|
238
|
-
} catch (error) {
|
|
239
|
-
console.error('Error checking migration reminder:', error);
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Mark the migration reminder as dismissed
|
|
246
|
-
*/
|
|
247
|
-
export function dismissMigrationReminder(): void {
|
|
248
|
-
try {
|
|
249
|
-
localStorage.setItem(MIGRATION_REMINDER_KEY, new Date().toISOString());
|
|
250
|
-
} catch (error) {
|
|
251
|
-
console.error('Error dismissing migration reminder:', error);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Clear the migration reminder dismissal (useful after successful migration)
|
|
257
|
-
*/
|
|
258
|
-
export function clearMigrationReminderDismissal(): void {
|
|
259
|
-
try {
|
|
260
|
-
localStorage.removeItem(MIGRATION_REMINDER_KEY);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
console.error('Error clearing migration reminder:', error);
|
|
263
|
-
}
|
|
264
|
-
}
|
package/src/lib/sounds.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sound and Haptic Manager for Email Automator
|
|
3
|
-
* Uses Web Audio API to synthesize sounds without external assets.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class SoundManager {
|
|
7
|
-
private ctx: AudioContext | null = null;
|
|
8
|
-
private enabled: boolean = false;
|
|
9
|
-
|
|
10
|
-
constructor() {
|
|
11
|
-
// Initialize enabled state from localStorage
|
|
12
|
-
this.enabled = localStorage.getItem('ea_sounds_enabled') === 'true';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
setEnabled(enabled: boolean) {
|
|
16
|
-
this.enabled = enabled;
|
|
17
|
-
localStorage.setItem('ea_sounds_enabled', enabled ? 'true' : 'none');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
isEnabled(): boolean {
|
|
21
|
-
return this.enabled;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private initCtx() {
|
|
25
|
-
if (!this.ctx) {
|
|
26
|
-
this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
27
|
-
}
|
|
28
|
-
if (this.ctx.state === 'suspended') {
|
|
29
|
-
this.ctx.resume();
|
|
30
|
-
}
|
|
31
|
-
return this.ctx;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Subtle haptic feedback for supported devices
|
|
36
|
-
*/
|
|
37
|
-
haptic(pattern: number | number[] = 10) {
|
|
38
|
-
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
|
39
|
-
navigator.vibrate(pattern);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Soft chime for new emails
|
|
45
|
-
*/
|
|
46
|
-
playNotify() {
|
|
47
|
-
if (!this.enabled) return;
|
|
48
|
-
const ctx = this.initCtx();
|
|
49
|
-
const osc = ctx.createOscillator();
|
|
50
|
-
const gain = ctx.createGain();
|
|
51
|
-
|
|
52
|
-
osc.type = 'sine';
|
|
53
|
-
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
|
|
54
|
-
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1);
|
|
55
|
-
|
|
56
|
-
gain.gain.setValueAtTime(0, ctx.currentTime);
|
|
57
|
-
gain.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.01);
|
|
58
|
-
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
|
59
|
-
|
|
60
|
-
osc.connect(gain);
|
|
61
|
-
gain.connect(ctx.destination);
|
|
62
|
-
|
|
63
|
-
osc.start();
|
|
64
|
-
osc.stop(ctx.currentTime + 0.3);
|
|
65
|
-
this.haptic(10);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Distinct tone for High Priority emails
|
|
70
|
-
*/
|
|
71
|
-
playAlert() {
|
|
72
|
-
if (!this.enabled) return;
|
|
73
|
-
const ctx = this.initCtx();
|
|
74
|
-
|
|
75
|
-
const playTone = (freq: number, start: number) => {
|
|
76
|
-
const osc = ctx.createOscillator();
|
|
77
|
-
const gain = ctx.createGain();
|
|
78
|
-
osc.type = 'sine';
|
|
79
|
-
osc.frequency.setValueAtTime(freq, ctx.currentTime + start);
|
|
80
|
-
gain.gain.setValueAtTime(0, ctx.currentTime + start);
|
|
81
|
-
gain.gain.linearRampToValueAtTime(0.1, ctx.currentTime + start + 0.05);
|
|
82
|
-
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + 0.4);
|
|
83
|
-
osc.connect(gain);
|
|
84
|
-
gain.connect(ctx.destination);
|
|
85
|
-
osc.start(ctx.currentTime + start);
|
|
86
|
-
osc.stop(ctx.currentTime + start + 0.4);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
playTone(660, 0); // E5
|
|
90
|
-
playTone(880, 0.1); // A5
|
|
91
|
-
this.haptic([20, 50, 20]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Soft success sound for completed actions
|
|
96
|
-
*/
|
|
97
|
-
playSuccess() {
|
|
98
|
-
if (!this.enabled) return;
|
|
99
|
-
const ctx = this.initCtx();
|
|
100
|
-
const osc = ctx.createOscillator();
|
|
101
|
-
const gain = ctx.createGain();
|
|
102
|
-
|
|
103
|
-
osc.type = 'triangle';
|
|
104
|
-
osc.frequency.setValueAtTime(523.25, ctx.currentTime); // C5
|
|
105
|
-
osc.frequency.exponentialRampToValueAtTime(1046.50, ctx.currentTime + 0.1); // C6
|
|
106
|
-
|
|
107
|
-
gain.gain.setValueAtTime(0, ctx.currentTime);
|
|
108
|
-
gain.gain.linearRampToValueAtTime(0.05, ctx.currentTime + 0.02);
|
|
109
|
-
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
|
|
110
|
-
|
|
111
|
-
osc.connect(gain);
|
|
112
|
-
gain.connect(ctx.destination);
|
|
113
|
-
|
|
114
|
-
osc.start();
|
|
115
|
-
osc.stop(ctx.currentTime + 0.2);
|
|
116
|
-
this.haptic(15);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export const sounds = new SoundManager();
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createClient } from '@supabase/supabase-js';
|
|
2
|
-
|
|
3
|
-
const STORAGE_KEY = 'email_automator_supabase_config';
|
|
4
|
-
|
|
5
|
-
export interface SupabaseConfig {
|
|
6
|
-
url: string;
|
|
7
|
-
anonKey: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function getSupabaseConfig(): SupabaseConfig | null {
|
|
11
|
-
try {
|
|
12
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
13
|
-
if (stored) {
|
|
14
|
-
return JSON.parse(stored);
|
|
15
|
-
}
|
|
16
|
-
} catch (error) {
|
|
17
|
-
console.error('Error reading Supabase config:', error);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Fallback to env vars
|
|
21
|
-
const url = import.meta.env.VITE_SUPABASE_URL;
|
|
22
|
-
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
url &&
|
|
26
|
-
anonKey &&
|
|
27
|
-
url !== 'your_supabase_url' &&
|
|
28
|
-
anonKey !== 'your_supabase_anon_key' &&
|
|
29
|
-
url.startsWith('http')
|
|
30
|
-
) {
|
|
31
|
-
return { url, anonKey };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function saveSupabaseConfig(config: SupabaseConfig): void {
|
|
38
|
-
try {
|
|
39
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error('Error saving Supabase config:', error);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function clearSupabaseConfig(): void {
|
|
46
|
-
try {
|
|
47
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error('Error clearing Supabase config:', error);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Validate Supabase connection
|
|
55
|
-
*/
|
|
56
|
-
export async function validateSupabaseConnection(
|
|
57
|
-
url: string,
|
|
58
|
-
anonKey: string
|
|
59
|
-
): Promise<{ valid: boolean; error?: string }> {
|
|
60
|
-
try {
|
|
61
|
-
// Basic URL validation
|
|
62
|
-
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
63
|
-
return { valid: false, error: 'Invalid URL format' };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Basic API key validation (supports both JWT anon keys and publishable keys)
|
|
67
|
-
const isJwtKey = anonKey.startsWith('eyJ');
|
|
68
|
-
const isPublishableKey = anonKey.startsWith('sb_publishable_');
|
|
69
|
-
|
|
70
|
-
if (!isJwtKey && !isPublishableKey) {
|
|
71
|
-
return { valid: false, error: 'Invalid API key format (must be anon or publishable key)' };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Skip network validation for publishable keys as they might not be standard JWTs
|
|
75
|
-
// compatible with the Postgrest root endpoint check
|
|
76
|
-
if (isPublishableKey) {
|
|
77
|
-
return { valid: true };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Test connection by making a simple request
|
|
81
|
-
const response = await fetch(`${url}/rest/v1/`, {
|
|
82
|
-
method: 'GET',
|
|
83
|
-
headers: {
|
|
84
|
-
apikey: anonKey,
|
|
85
|
-
Authorization: `Bearer ${anonKey}`,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (!response.ok) {
|
|
90
|
-
if (response.status === 401 || response.status === 403) {
|
|
91
|
-
return { valid: false, error: 'Invalid API key' };
|
|
92
|
-
}
|
|
93
|
-
return { valid: false, error: `Connection failed: ${response.statusText}` };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { valid: true };
|
|
97
|
-
} catch (error) {
|
|
98
|
-
return {
|
|
99
|
-
valid: false,
|
|
100
|
-
error: error instanceof Error ? error.message : 'Connection failed',
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get Supabase config source (for display purposes)
|
|
107
|
-
*/
|
|
108
|
-
export function getConfigSource(): 'ui' | 'env' | 'none' {
|
|
109
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
110
|
-
if (stored) return 'ui';
|
|
111
|
-
|
|
112
|
-
const url = import.meta.env.VITE_SUPABASE_URL;
|
|
113
|
-
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
114
|
-
if (url && anonKey) return 'env';
|
|
115
|
-
|
|
116
|
-
return 'none';
|
|
117
|
-
}
|
package/src/lib/supabase.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
import { getSupabaseConfig } from './supabase-config';
|
|
3
|
-
|
|
4
|
-
// Create client immediately to ensure session restoration happens early
|
|
5
|
-
// This is critical for auth session persistence across page refreshes
|
|
6
|
-
function createSupabaseClient(): SupabaseClient {
|
|
7
|
-
const config = getSupabaseConfig();
|
|
8
|
-
|
|
9
|
-
if (!config || !config.url?.startsWith('http')) {
|
|
10
|
-
// Return a placeholder client that will never be used
|
|
11
|
-
// (App.tsx will show setup wizard before this is accessed)
|
|
12
|
-
console.info('[Supabase] Setup required: Waiting for configuration...');
|
|
13
|
-
return createClient('https://placeholder.supabase.co', 'placeholder-key');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return createClient(config.url, config.anonKey, {
|
|
17
|
-
auth: {
|
|
18
|
-
// Ensure session is persisted and restored from localStorage
|
|
19
|
-
persistSession: true,
|
|
20
|
-
autoRefreshToken: true,
|
|
21
|
-
detectSessionInUrl: true,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Create client immediately on module load (not lazy!)
|
|
27
|
-
// This ensures session restoration from localStorage happens before any auth checks
|
|
28
|
-
export const supabase = createSupabaseClient();
|
package/src/lib/types.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// Database types
|
|
2
|
-
export interface Profile {
|
|
3
|
-
id: string;
|
|
4
|
-
first_name: string | null;
|
|
5
|
-
last_name: string | null;
|
|
6
|
-
email: string | null;
|
|
7
|
-
avatar_url: string | null;
|
|
8
|
-
is_admin: boolean;
|
|
9
|
-
created_at: string;
|
|
10
|
-
updated_at: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface EmailAccount {
|
|
14
|
-
id: string;
|
|
15
|
-
user_id: string;
|
|
16
|
-
provider: 'gmail' | 'outlook';
|
|
17
|
-
email_address: string;
|
|
18
|
-
is_active: boolean;
|
|
19
|
-
last_sync_checkpoint?: string | null;
|
|
20
|
-
sync_start_date?: string | null;
|
|
21
|
-
sync_max_emails_per_run?: number;
|
|
22
|
-
last_sync_at?: string | null;
|
|
23
|
-
last_sync_status?: 'idle' | 'syncing' | 'success' | 'error';
|
|
24
|
-
last_sync_error?: string | null;
|
|
25
|
-
created_at: string;
|
|
26
|
-
updated_at: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface Email {
|
|
30
|
-
id: string;
|
|
31
|
-
account_id: string;
|
|
32
|
-
external_id: string;
|
|
33
|
-
subject: string | null;
|
|
34
|
-
sender: string | null;
|
|
35
|
-
recipient: string | null;
|
|
36
|
-
date: string | null;
|
|
37
|
-
body_snippet: string | null;
|
|
38
|
-
category: EmailCategory | null;
|
|
39
|
-
is_useless: boolean;
|
|
40
|
-
ai_analysis: EmailAnalysis | null;
|
|
41
|
-
suggested_action?: EmailAction | null; // Deprecated
|
|
42
|
-
suggested_actions?: EmailAction[];
|
|
43
|
-
action_taken?: EmailAction | null; // Deprecated
|
|
44
|
-
actions_taken?: EmailAction[];
|
|
45
|
-
created_at: string;
|
|
46
|
-
email_accounts?: EmailAccount;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface Rule {
|
|
50
|
-
id: string;
|
|
51
|
-
user_id: string;
|
|
52
|
-
name: string;
|
|
53
|
-
condition: RuleCondition;
|
|
54
|
-
action: 'delete' | 'archive' | 'draft' | 'read' | 'star';
|
|
55
|
-
instructions?: string;
|
|
56
|
-
attachments?: RuleAttachment[];
|
|
57
|
-
is_enabled: boolean;
|
|
58
|
-
is_system?: boolean; // New flag for pre-defined rules
|
|
59
|
-
created_at: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface RuleCondition {
|
|
63
|
-
category?: EmailCategory;
|
|
64
|
-
is_useless?: boolean;
|
|
65
|
-
sender_contains?: string;
|
|
66
|
-
sender_domain?: string;
|
|
67
|
-
sender_email?: string;
|
|
68
|
-
subject_contains?: string;
|
|
69
|
-
body_contains?: string;
|
|
70
|
-
older_than_days?: number;
|
|
71
|
-
priority?: Priority;
|
|
72
|
-
sentiment?: Sentiment;
|
|
73
|
-
suggested_actions?: EmailAction[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface RuleAttachment {
|
|
77
|
-
name: string;
|
|
78
|
-
path: string;
|
|
79
|
-
type: string;
|
|
80
|
-
size: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface UserSettings {
|
|
84
|
-
id?: string;
|
|
85
|
-
user_id?: string;
|
|
86
|
-
llm_model: string | null;
|
|
87
|
-
llm_base_url: string | null;
|
|
88
|
-
llm_api_key: string | null;
|
|
89
|
-
sync_interval_minutes: number;
|
|
90
|
-
preferences?: Record<string, any>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface Integration {
|
|
94
|
-
id: string;
|
|
95
|
-
user_id: string;
|
|
96
|
-
provider: 'google' | 'microsoft' | 'openai';
|
|
97
|
-
credentials: Record<string, any>;
|
|
98
|
-
is_enabled: boolean;
|
|
99
|
-
created_at: string;
|
|
100
|
-
updated_at: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface ProcessingLog {
|
|
104
|
-
id: string;
|
|
105
|
-
user_id: string;
|
|
106
|
-
status: 'running' | 'success' | 'failed';
|
|
107
|
-
started_at: string;
|
|
108
|
-
completed_at: string | null;
|
|
109
|
-
emails_processed: number;
|
|
110
|
-
emails_deleted: number;
|
|
111
|
-
emails_drafted: number;
|
|
112
|
-
error_message: string | null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface ProcessingEvent {
|
|
116
|
-
id: string;
|
|
117
|
-
run_id: string;
|
|
118
|
-
email_id?: string | null;
|
|
119
|
-
event_type: 'info' | 'analysis' | 'action' | 'error';
|
|
120
|
-
agent_state: string;
|
|
121
|
-
details?: any;
|
|
122
|
-
created_at: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Enums
|
|
126
|
-
export type EmailCategory = 'spam' | 'newsletter' | 'support' | 'client' | 'internal' | 'personal' | 'other';
|
|
127
|
-
export type EmailAction = 'none' | 'delete' | 'archive' | 'reply' | 'flag' | 'draft';
|
|
128
|
-
export type Sentiment = 'Positive' | 'Neutral' | 'Negative';
|
|
129
|
-
export type Priority = 'High' | 'Medium' | 'Low';
|
|
130
|
-
|
|
131
|
-
// AI Analysis
|
|
132
|
-
export interface EmailAnalysis {
|
|
133
|
-
summary: string;
|
|
134
|
-
category: EmailCategory;
|
|
135
|
-
sentiment: Sentiment;
|
|
136
|
-
is_useless: boolean;
|
|
137
|
-
suggested_action: EmailAction;
|
|
138
|
-
draft_response?: string;
|
|
139
|
-
priority: Priority;
|
|
140
|
-
key_points?: string[];
|
|
141
|
-
action_items?: string[];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Stats
|
|
145
|
-
export interface Stats {
|
|
146
|
-
totalEmails: number;
|
|
147
|
-
categoryCounts: Record<string, number>;
|
|
148
|
-
actionCounts: Record<string, number>;
|
|
149
|
-
uselessCount: number;
|
|
150
|
-
accountCount: number;
|
|
151
|
-
accountsByProvider: Record<string, number>;
|
|
152
|
-
recentSyncs: ProcessingLog[];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// API Response types
|
|
156
|
-
export interface ApiError {
|
|
157
|
-
code: string;
|
|
158
|
-
message: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface PaginatedResponse<T> {
|
|
162
|
-
data: T[];
|
|
163
|
-
total: number;
|
|
164
|
-
limit: number;
|
|
165
|
-
offset: number;
|
|
166
|
-
}
|