From 5ab5698fe2aacff7138b09bac9bb166e2453b6a9 Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Mon, 24 Nov 2025 19:41:41 -0800 Subject: [PATCH] working ! testing ! --- .gitea/workflows/deploy.yml | 3 ++- src/routes/ai.ts | 12 +----------- src/services/aiApiClient.ts | 24 +++++++++++++----------- src/services/db/flyer.ts | 16 +++++++++------- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 756ad40f..bbed61f6 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -180,7 +180,8 @@ jobs: DEPLOYED_HASH=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_DATABASE" -c "SELECT schema_hash FROM public.schema_info WHERE id = 1;" -t -A || echo "none") echo "Deployed DB Schema Hash: $DEPLOYED_HASH" - if [ "$DEPLOYED_HASH" = "none" ]; then + # Check if the hash is "none" (command failed) OR if it's an empty string (table exists but is empty). + if [ "$DEPLOYED_HASH" = "none" ] || [ -z "$DEPLOYED_HASH" ]; then echo "WARNING: No schema hash found in the production database." echo "This is expected for a first-time deployment. The hash will be set after a successful deployment." # We allow the deployment to continue, but a manual schema update is required. diff --git a/src/routes/ai.ts b/src/routes/ai.ts index 86b48b9e..ef85edf7 100644 --- a/src/routes/ai.ts +++ b/src/routes/ai.ts @@ -85,23 +85,13 @@ router.post('/flyers/process', optionalAuth, upload.single('flyerImage'), async return res.status(409).json({ message: 'This flyer has already been processed.' }); } - // Find or create the store to get its ID, which is required by `createFlyerAndItems`. - let storeId: number; - const storeRes = await db.getPool().query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [extractedData.store_name]); - if (storeRes.rows.length > 0) { - storeId = storeRes.rows[0].store_id; - } else { - const newStoreRes = await db.getPool().query<{ store_id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id', [extractedData.store_name]); - storeId = newStoreRes.rows[0].store_id; - } - // 2. Prepare flyer data for insertion const flyerData = { file_name: originalFileName, image_url: `/assets/${req.file.filename}`, checksum: checksum, + // Pass the store_name directly to the DB function. store_name: extractedData.store_name, - store_id: storeId, // Use the resolved store_id valid_from: extractedData.valid_from, valid_to: extractedData.valid_to, store_address: extractedData.store_address, diff --git a/src/services/aiApiClient.ts b/src/services/aiApiClient.ts index 883bfbf6..8ee272e4 100644 --- a/src/services/aiApiClient.ts +++ b/src/services/aiApiClient.ts @@ -7,13 +7,15 @@ import type { GroundingChunk } from "@google/genai"; import type { FlyerItem, MasterGroceryItem, Store, ExtractedCoreData, ExtractedLogoData } from "../types"; import { logger } from "./logger"; -import { apiFetch } from './apiClient'; // Use the authenticated fetch wrapper +import { apiFetchWithAuth } from './apiClient'; export const isImageAFlyer = async (imageFile: File): Promise => { const formData = new FormData(); formData.append('image', imageFile); - const response = await apiFetch('/ai/check-flyer', { + // Use apiFetchWithAuth for FormData to let the browser set the correct Content-Type. + // The URL must be relative, as the helper constructs the full path. + const response = await apiFetchWithAuth('/ai/check-flyer', { method: 'POST', body: formData, }); @@ -25,7 +27,7 @@ export const extractAddressFromImage = async (imageFile: File): Promise => { - const response = await apiFetch('/ai/quick-insights', { + const response = await apiFetchWithAuth('/ai/quick-insights', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }), @@ -90,7 +92,7 @@ export const getQuickInsights = async (items: FlyerItem[]): Promise => { }; export const getDeepDiveAnalysis = async (items: FlyerItem[]): Promise => { - const response = await apiFetch('/ai/deep-dive', { + const response = await apiFetchWithAuth('/ai/deep-dive', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }), @@ -100,7 +102,7 @@ export const getDeepDiveAnalysis = async (items: FlyerItem[]): Promise = }; export const searchWeb = async (items: FlyerItem[]): Promise<{text: string; sources: GroundingChunk[]}> => { - const response = await apiFetch('/ai/search-web', { + const response = await apiFetchWithAuth('/ai/search-web', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }), @@ -121,7 +123,7 @@ export const searchWeb = async (items: FlyerItem[]): Promise<{text: string; sour */ export const planTripWithMaps = async (items: FlyerItem[], store: Store | undefined, userLocation: GeolocationCoordinates): Promise<{text: string; sources: { uri: string; title: string; }[]}> => { logger.debug("Stub: planTripWithMaps called with location:", { userLocation }); - const response = await apiFetch('/ai/plan-trip', { + const response = await apiFetchWithAuth('/ai/plan-trip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items, store, userLocation }), @@ -136,7 +138,7 @@ export const planTripWithMaps = async (items: FlyerItem[], store: Store | undefi */ export const generateImageFromText = async (prompt: string): Promise => { logger.debug("Stub: generateImageFromText called with prompt:", { prompt }); - const response = await apiFetch('/ai/generate-image', { + const response = await apiFetchWithAuth('/ai/generate-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }), @@ -152,7 +154,7 @@ export const generateImageFromText = async (prompt: string): Promise => */ export const generateSpeechFromText = async (text: string): Promise => { logger.debug("Stub: generateSpeechFromText called with text:", { text }); - const response = await apiFetch('/ai/generate-speech', { + const response = await apiFetchWithAuth('/ai/generate-speech', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), diff --git a/src/services/db/flyer.ts b/src/services/db/flyer.ts index 44f95cc4..678dc427 100644 --- a/src/services/db/flyer.ts +++ b/src/services/db/flyer.ts @@ -126,7 +126,7 @@ export async function findFlyerByChecksum(checksum: string): Promise, + flyerData: Omit & { store_name: string }, items: Omit[] ): Promise { const client = await getPool().connect(); @@ -135,12 +135,14 @@ export async function createFlyerAndItems( await client.query('BEGIN'); logger.debug('[DB createFlyerAndItems] BEGIN transaction successful.'); - // Find or create the store - // The store_id is now expected to be passed in the flyerData object, - // as it's resolved in the route handler. - const storeId = flyerData.store_id; - if (!storeId) { - throw new Error("store_id is required to create a flyer."); + // Find or create the store to get its ID. This logic is now self-contained. + let storeId: number; + const storeRes = await client.query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [flyerData.store_name]); + if (storeRes.rows.length > 0) { + storeId = storeRes.rows[0].store_id; + } else { + const newStoreRes = await client.query<{ store_id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id', [flyerData.store_name]); + storeId = newStoreRes.rows[0].store_id; } // Create the flyer record