some more re-org + fixes
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m55s

This commit is contained in:
2025-11-24 15:02:13 -08:00
parent a4d5e95937
commit 2f55a303b0
5 changed files with 40 additions and 24 deletions

11
package-lock.json generated
View File

@@ -51,6 +51,7 @@
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"@types/react": "^19.2.7",
"@types/zxcvbn": "^4.4.5", "@types/zxcvbn": "^4.4.5",
"@typescript-eslint/eslint-plugin": "^8.47.0", "@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0", "@typescript-eslint/parser": "^8.47.0",
@@ -4426,6 +4427,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/send": { "node_modules/@types/send": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",

View File

@@ -63,6 +63,7 @@
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"@types/react": "^19.2.7",
"@types/zxcvbn": "^4.4.5", "@types/zxcvbn": "^4.4.5",
"@typescript-eslint/eslint-plugin": "^8.47.0", "@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0", "@typescript-eslint/parser": "^8.47.0",

View File

@@ -352,14 +352,14 @@ function App() {
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const validFlyers = flyers.filter(flyer => { const validFlyers = flyers.filter((flyer: Flyer) => {
if (!flyer.valid_from || !flyer.valid_to) return false; if (!flyer.valid_from || !flyer.valid_to) return false;
try { try {
const from = new Date(`${flyer.valid_from}T00:00:00`); const from = new Date(`${flyer.valid_from}T00:00:00`);
const to = new Date(`${flyer.valid_to}T00:00:00`); const to = new Date(`${flyer.valid_to}T00:00:00`);
return today >= from && today <= to; return today >= from && today <= to;
} catch (e) { } catch (e) {
logger.error("Error parsing flyer date", e); logger.error("Error parsing flyer date", { error: e });
return false; return false;
} }
}); });
@@ -372,12 +372,12 @@ function App() {
const validFlyerIds = validFlyers.map(f => f.flyer_id); const validFlyerIds = validFlyers.map(f => f.flyer_id);
const allItems = await apiFetchFlyerItemsForFlyers(validFlyerIds); const allItems = await apiFetchFlyerItemsForFlyers(validFlyerIds);
const watchedItemIds = new Set(watchedItems.map(item => item.item_id)); const watchedItemIds = new Set(watchedItems.map((item: MasterGroceryItem) => item.master_grocery_item_id));
const dealItemsRaw = allItems.filter(item => const dealItemsRaw = allItems.filter(item =>
item.master_item_id && watchedItemIds.has(item.master_item_id) item.master_item_id && watchedItemIds.has(item.master_item_id)
); // This seems correct as it's comparing with master_item_id ); // This seems correct as it's comparing with master_item_id
const flyerIdToStoreName = new Map(validFlyers.map(f => [f.flyer_id, f.store?.name || 'Unknown Store'])); const flyerIdToStoreName = new Map(validFlyers.map((f: Flyer) => [f.flyer_id, f.store?.name || 'Unknown Store']));
const deals: DealItem[] = dealItemsRaw.map(item => ({ const deals: DealItem[] = dealItemsRaw.map(item => ({
item: item.item, item: item.item,
@@ -412,14 +412,14 @@ function App() {
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const validFlyers = flyers.filter(flyer => { const validFlyers = flyers.filter((flyer: Flyer) => {
if (!flyer.valid_from || !flyer.valid_to) return false; if (!flyer.valid_from || !flyer.valid_to) return false;
try { try {
const from = new Date(`${flyer.valid_from}T00:00:00`); const from = new Date(`${flyer.valid_from}T00:00:00`);
const to = new Date(`${flyer.valid_to}T00:00:00`); const to = new Date(`${flyer.valid_to}T00:00:00`);
return today >= from && today <= to; return today >= from && today <= to;
} catch (e) { } catch (e) {
logger.error("Error parsing flyer date", e); logger.error("Error parsing flyer date", { error: e });
return false; return false;
} }
}); });
@@ -494,6 +494,11 @@ function App() {
} }
} }
// If extractedData is null or undefined at this point, it means the AI call failed.
if (!extractedData) {
throw new Error("Core data extraction failed. The AI did not return valid data.");
}
const { store_name, valid_from, valid_to, items: extractedItems } = extractedData; const { store_name, valid_from, valid_to, items: extractedItems } = extractedData;
stageIndex += 2; // Increment by 2 for the stages we just completed. stageIndex is now 3 stageIndex += 2; // Increment by 2 for the stages we just completed. stageIndex is now 3
@@ -657,9 +662,8 @@ function App() {
try { try {
const updatedOrNewItem = await apiAddWatchedItem(itemName, category); const updatedOrNewItem = await apiAddWatchedItem(itemName, category);
setWatchedItems(prevItems => { setWatchedItems(prevItems => {
// The API returns an object with `id`, but our state uses `item_id`. // Check if the item already exists in the state by its correct ID property.
// We compare the existing item's `item_id` with the new item's `id`. const itemExists = prevItems.some(item => item.master_grocery_item_id === updatedOrNewItem.master_grocery_item_id);
const itemExists = prevItems.some(item => item.item_id === (updatedOrNewItem as any).id);
if (!itemExists) { if (!itemExists) {
const newItems = [...prevItems, updatedOrNewItem]; // This was correct, but the check above was wrong. const newItems = [...prevItems, updatedOrNewItem]; // This was correct, but the check above was wrong.
return newItems.sort((a,b) => a.name.localeCompare(b.name)); return newItems.sort((a,b) => a.name.localeCompare(b.name));
@@ -676,8 +680,8 @@ function App() {
const handleRemoveWatchedItem = useCallback(async (masterItemId: number) => { const handleRemoveWatchedItem = useCallback(async (masterItemId: number) => {
if (!user) return; if (!user) return;
try { try {
await apiRemoveWatchedItem(masterItemId); await apiRemoveWatchedItem(masterItemId); // API call is correct
setWatchedItems(prevItems => prevItems.filter(item => item.item_id !== masterItemId)); setWatchedItems(prevItems => prevItems.filter(item => item.master_grocery_item_id !== masterItemId)); // State update must use correct property
} catch (e) { } catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e); const errorMessage = e instanceof Error ? e.message : String(e);
setError(`Could not remove watched item: ${errorMessage}`); setError(`Could not remove watched item: ${errorMessage}`);
@@ -718,9 +722,8 @@ function App() {
setShoppingLists(prevLists => prevLists.map(list => { setShoppingLists(prevLists => prevLists.map(list => {
if (list.shopping_list_id === listId) { if (list.shopping_list_id === listId) {
// Avoid adding duplicates to the state if it's already there // Avoid adding duplicates to the state if it's already there
// The API returns an object with `id`, but our state uses `item_id`. // Check if the item already exists in the list by its correct ID property.
// We compare the existing item's `item_id` with the new item's `id`. const itemExists = list.items.some(i => i.shopping_list_item_id === newItem.shopping_list_item_id);
const itemExists = list.items.some(i => i.item_id === (newItem as any).id);
if (itemExists) return list; if (itemExists) return list;
return { ...list, items: [...list.items, newItem] }; return { ...list, items: [...list.items, newItem] };
} }
@@ -738,7 +741,7 @@ function App() {
const updatedItem = await apiUpdateShoppingListItem(itemId, updates); const updatedItem = await apiUpdateShoppingListItem(itemId, updates);
setShoppingLists(prevLists => prevLists.map(list => { setShoppingLists(prevLists => prevLists.map(list => {
if (list.shopping_list_id === activeListId) { if (list.shopping_list_id === activeListId) {
return { ...list, items: list.items.map(i => i.item_id === itemId ? updatedItem : i) }; return { ...list, items: list.items.map(i => i.shopping_list_item_id === itemId ? updatedItem : i) };
} }
return list; return list;
})); }));
@@ -754,7 +757,7 @@ function App() {
await apiRemoveShoppingListItem(itemId); await apiRemoveShoppingListItem(itemId);
setShoppingLists(prevLists => prevLists.map(list => { setShoppingLists(prevLists => prevLists.map(list => {
if (list.shopping_list_id === activeListId) { if (list.shopping_list_id === activeListId) {
return { ...list, items: list.items.filter(i => i.item_id !== itemId) }; return { ...list, items: list.items.filter(i => i.shopping_list_item_id !== itemId) };
} }
return list; return list;
})); }));
@@ -772,10 +775,10 @@ function App() {
}; };
const handleActivityLogClick: ActivityLogClickHandler = (log) => { const handleActivityLogClick: ActivityLogClickHandler = (log) => {
if (log.activity_type === 'share_shopping_list' && log.entity_id) { if (log.action === 'list_shared' && log.details?.shopping_list_id) {
const listId = parseInt(log.entity_id, 10); const listId = parseInt(String(log.details.shopping_list_id), 10);
// Check if the list exists before setting it as active. This was correct. // Check if the list exists before setting it as active. This was correct.
if (shoppingLists.some(list => list.shopping_list_id === listId)) { if (shoppingLists.some(list => list.shopping_list_id === listId) && typeof listId === 'number') {
setActiveListId(listId); setActiveListId(listId);
} }
} }
@@ -880,7 +883,7 @@ function App() {
</div> </div>
<div className="lg:col-span-2 flex flex-col space-y-6"> <div className="lg:col-span-2 flex flex-col space-y-6">
<ErrorDisplay message={error} /> {error && <ErrorDisplay message={error} />}
{isProcessing ? ( {isProcessing ? (
<ProcessingStatus <ProcessingStatus

View File

@@ -302,7 +302,7 @@ export const processFlyerFile = async (
flyerImage: File, flyerImage: File,
checksum: string, checksum: string,
originalFileName: string, originalFileName: string,
extractedData: { store_name: string; valid_from: string; valid_to: string; items: Omit<FlyerItem, 'id' | 'flyer_id' | 'created_at'>[]; store_address: string | null } extractedData: { store_name: string; valid_from: string | null; valid_to: string | null; items: Omit<FlyerItem, 'flyer_item_id' | 'flyer_id' | 'created_at'>[]; store_address: string | null }
): Promise<{ message: string; flyer: Flyer }> => { ): Promise<{ message: string; flyer: Flyer }> => {
const formData = new FormData(); const formData = new FormData();
formData.append('flyerImage', flyerImage); formData.append('flyerImage', flyerImage);
@@ -1293,10 +1293,10 @@ export async function deleteUserAccount(password: string, tokenOverride?: string
try { try {
const data = await response.json(); const data = await response.json();
errorMessage = data.message || errorMessage; errorMessage = data.message || errorMessage;
} catch (e) { } catch (jsonError) {
// Response was not JSON (likely 404 HTML or 500 text) // Response was not JSON (likely 404 HTML or 500 text)
const text = await response.clone().text(); // Use clone() to avoid "Body already read" error const text = await response.clone().text(); // Use clone() to avoid "Body already read" error
logger.error(`deleteUserAccount failed with status ${response.status}. Body: ${text}`); logger.error(`deleteUserAccount failed with status ${response.status}. Could not parse JSON response. Body: ${text}`, { jsonError });
errorMessage += `: ${response.status} ${response.statusText}`; errorMessage += `: ${response.status} ${response.statusText}`;
} }
throw new Error(errorMessage); throw new Error(errorMessage);

View File

@@ -14,7 +14,8 @@ const { test: _unusedTest, ...baseViteConfig } = viteConfig as any;
console.error('\n[DEBUG] --- INTEGRATION CONFIG SETUP ---'); console.error('\n[DEBUG] --- INTEGRATION CONFIG SETUP ---');
// Use _unusedTest to satisfy linter // Use _unusedTest to satisfy linter
console.error(`[DEBUG] Stripped "test" config. Original test config existed: ${!!_unusedTest}`); console.error(`[DEBUG] Stripped "test" config. Original test config existed: ${!!_unusedTest}`);
console.error('[DEBUG] Does baseViteConfig have "test"?', !!(baseViteConfig as any).test); // Use the 'in' operator for a type-safe property check instead of casting to 'any'.
console.error('[DEBUG] Does baseViteConfig have "test"?', 'test' in baseViteConfig);
/** /**
* This configuration is specifically for integration tests. * This configuration is specifically for integration tests.