logging etc - testing signup flow still
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 20s

This commit is contained in:
2025-11-10 21:41:58 -08:00
parent f76994f8ba
commit 76286bdb50
4 changed files with 83 additions and 15 deletions

36
App.tsx
View File

@@ -5,6 +5,7 @@ import { AnalysisPanel } from './components/AnalysisPanel';
import { PriceChart } from './components/PriceChart';
import { ErrorDisplay } from './components/ErrorDisplay';
import { Header } from './components/Header';
import { logger } from './services/logger';
import { isImageAFlyer, extractCoreDataFromImage, extractAddressFromImage, extractLogoFromImage } from './services/geminiService';
import type { FlyerItem, Flyer, MasterGroceryItem, DealItem, ProcessingStage, StageStatus, Store, Profile, ShoppingList, ShoppingListItem } from './types';
import type { Database } from './types/supabase'; // Correctly import the Database type
@@ -185,11 +186,13 @@ function App() {
// It fetches user-specific data when a session is established.
const fetchRealUserSessionData = async (session: Session | null) => {
setSession(session);
logger.info('Auth state change detected.', { hasSession: !!session });
if (session) {
const userProfile = await getUserProfile(session.user.id);
setProfile(userProfile);
fetchWatchedItems(session.user.id);
fetchShoppingLists(session.user.id);
logger.info('User session active. Fetched profile and user data.', { userId: session.user.id });
} else {
setProfile(null);
setWatchedItems([]);
@@ -203,13 +206,7 @@ function App() {
// Add a separate listener for specific auth events to provide user feedback.
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
// When a user signs up, they are immediately signed in, but their profile
// is not yet complete. This is a good time to open the profile manager.
if (event === "SIGNED_IN" && session && !profile) {
console.log("New user signed in, opening profile manager.");
setIsProfileManagerOpen(true);
}
logger.info(`Supabase auth event: ${event}`);
if (event === "SIGNED_OUT") {
setIsProfileManagerOpen(false);
}
@@ -219,6 +216,17 @@ function App() {
return () => subscription.unsubscribe();
}, [isDbConnected, fetchWatchedItems, fetchShoppingLists]);
// Effect to handle the post-signup redirect.
// This is more reliable than checking inside onAuthStateChange, which can re-run.
useEffect(() => {
const hash = window.location.hash;
// If the URL contains '#...' and 'type=signup', it's a confirmation link click.
if (hash && hash.includes('type=signup')) {
logger.info("New user confirmed email, opening profile manager.");
setIsProfileManagerOpen(true);
}
}, []);
useEffect(() => {
if (isReady && isDbConnected) {
@@ -281,7 +289,7 @@ function App() {
const to = new Date(`${flyer.valid_to}T00:00:00`);
return today >= from && today <= to;
} catch (e) {
console.error("Error parsing flyer date", e);
logger.error("Error parsing flyer date", e);
return false;
}
});
@@ -340,7 +348,7 @@ function App() {
const to = new Date(`${flyer.valid_to}T00:00:00`);
return today >= from && today <= to;
} catch (e) {
console.error("Error parsing flyer date", e);
logger.error("Error parsing flyer date", e);
return false;
}
});
@@ -354,7 +362,7 @@ function App() {
const totalCount = await countFlyerItemsForFlyers(validFlyerIds);
setTotalActiveItems(totalCount);
} catch (e: any) {
console.error("Failed to calculate total active items:", e.message);
logger.error("Failed to calculate total active items:", { error: e.message });
setTotalActiveItems(0);
}
};
@@ -424,7 +432,7 @@ function App() {
storeAddress = await withTimeout(extractAddressFromImage(files[0]), nonCriticalTimeout);
updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 4
} catch (e: any) {
console.warn("Non-critical step failed: Address extraction.", e.message);
logger.warn("Non-critical step failed: Address extraction.", { error: e.message });
updateStage?.(stageIndex++, { status: 'error', detail: '(Skipped)' }); // stageIndex is now 4
}
@@ -436,7 +444,7 @@ function App() {
storeLogoBase64 = logoData.store_logo_base_64;
updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 5
} catch (e: any) {
console.warn("Non-critical step failed: Logo extraction.", e.message);
logger.warn("Non-critical step failed: Logo extraction.", { error: e.message });
updateStage?.(stageIndex++, { status: 'error', detail: '(Skipped)' }); // stageIndex is now 5
}
@@ -563,7 +571,7 @@ function App() {
checksum = await generateFileChecksum(originalFile);
const existing = await findFlyerByChecksum(checksum);
if (existing) {
console.log(`Skipping duplicate file: ${originalFile.name}`);
logger.info(`Skipping duplicate file: ${originalFile.name}`);
summary.skipped.push(originalFile.name);
updateStage(currentStageIndex, { status: 'completed', detail: '(Duplicate)' });
setProcessingProgress(((i + 1) / files.length) * 100);
@@ -577,7 +585,7 @@ function App() {
await processFiles(filesToProcess, checksum, originalFile.name, processFilesUpdateStage);
summary.processed.push(originalFile.name);
} catch (e: any) {
console.error(`Failed to process ${originalFile.name}:`, e);
logger.error(`Failed to process ${originalFile.name}:`, { error: e });
summary.errors.push({ fileName: originalFile.name, message: e.message });
setProcessingStages(prev => prev.map(stage => {
if (stage.status === 'in-progress' && (stage.critical ?? true)) {

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Session } from '@supabase/supabase-js';
import type { Profile } from '../types';
import { supabase, updateUserProfile, updateUserPassword, exportUserData, deleteUserAccount } from '../services/supabaseClient';
import { logger } from '../services/logger';
import { LoadingSpinner } from './LoadingSpinner';
import { XMarkIcon } from './icons/XMarkIcon';
import { GoogleIcon } from './icons/GoogleIcon';
@@ -63,8 +64,10 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
avatar_url: avatarUrl
});
onProfileUpdate(updatedProfile);
logger.info('User profile updated successfully.', { userId: session.user.id });
setProfileMessage('Profile updated successfully!');
} catch (error: any) {
logger.error('Failed to update user profile.', { userId: session.user.id, error: error.message });
setProfileMessage(error.message);
} finally {
setProfileLoading(false);
@@ -83,6 +86,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
});
if (error) {
// This error will be shown if the user cancels or if there's a config issue.
logger.error(`Could not link ${provider} account.`, { userId: session.user.id, error: error.message });
setPasswordError(`Could not link ${provider} account: ${error.message}`);
}
};
@@ -102,10 +106,12 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
setPasswordMessage('');
try {
await updateUserPassword(password);
logger.info('User password updated successfully.', { userId: session.user.id });
setPasswordMessage("Password updated successfully!");
setPassword('');
setConfirmPassword('');
} catch (error: any) {
logger.error('Failed to update user password.', { userId: session.user.id, error: error.message });
setPasswordError(error.message);
} finally {
setPasswordLoading(false);
@@ -119,6 +125,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
const handleExportData = async () => {
setExportLoading(true);
try {
logger.info('User initiated data export.', { userId: session.user.id });
const userData = await exportUserData(session.user.id);
const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(userData, null, 2))}`;
const link = document.createElement("a");
@@ -126,7 +133,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
link.download = `flyer-crawler-data-export-${new Date().toISOString().split('T')[0]}.json`;
link.click();
} catch (error: any) {
console.error("Failed to export data:", error);
logger.error("Failed to export user data:", { userId: session.user.id, error });
alert(`Error exporting data: ${error.message}`);
} finally {
setExportLoading(false);
@@ -138,11 +145,13 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
setDeleteLoading(true);
setDeleteError('');
try {
logger.warn('User initiated account deletion.', { userId: session.user.id });
await deleteUserAccount(passwordForDelete);
alert("Your account and all associated data have been permanently deleted.");
// The onAuthStateChange listener in App.tsx will handle the UI update
await supabase.auth.signOut();
onClose();
logger.warn('User account deleted successfully.', { userId: session.user.id });
} catch (error: any) {
setDeleteError(error.message);
} finally {

View File

@@ -15,6 +15,7 @@ interface SignUpModalProps {
export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwitchToSignIn }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
@@ -22,6 +23,13 @@ export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwi
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
// Add password confirmation check
if (password !== confirmPassword) {
setError("Passwords do not match.");
setLoading(false);
return;
}
setError(null);
setMessage(null);
@@ -111,6 +119,10 @@ export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwi
<input id="password-signup" type="password" value={password} onChange={e => setPassword(e.target.value)} required className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary" placeholder="••••••••" />
{password.length > 0 && <PasswordStrength password={password} />}
</div>
<div>
<label htmlFor="confirm-password-signup" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm Password</label>
<input id="confirm-password-signup" type="password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} required className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary" placeholder="••••••••" />
</div>
{error && <p className="text-sm text-red-600 dark:text-red-400 text-center">{error}</p>}
{message && <p className="text-sm text-green-600 dark:text-green-400 text-center">{message}</p>}
<button type="submit" disabled={loading || !!message} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold py-2.5 px-4 rounded-lg transition-colors duration-300 flex items-center justify-center">

39
services/logger.ts Normal file
View File

@@ -0,0 +1,39 @@
/**
* A simple logger service that wraps the console.
* This provides a centralized place to manage logging behavior,
* such as adding timestamps, log levels, or sending logs to a remote service.
*/
const getTimestamp = () => new Date().toISOString();
type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
const log = (level: LogLevel, message: string, ...args: any[]) => {
const timestamp = getTimestamp();
// We construct the log message with a timestamp and level for better context.
const logMessage = `[${timestamp}] [${level}] ${message}`;
switch (level) {
case 'INFO':
console.log(logMessage, ...args);
break;
case 'WARN':
console.warn(logMessage, ...args);
break;
case 'ERROR':
console.error(logMessage, ...args);
break;
case 'DEBUG':
// For now, we can show debug logs in development. This could be controlled by an environment variable.
console.debug(logMessage, ...args);
break;
}
};
// Export the logger object for use throughout the application.
export const logger = {
info: (message: string, ...args: any[]) => log('INFO', message, ...args),
warn: (message: string, ...args: any[]) => log('WARN', message, ...args),
error: (message: string, ...args: any[]) => log('ERROR', message, ...args),
debug: (message: string, ...args: any[]) => log('DEBUG', message, ...args),
};