working ! testing !
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 2m31s

This commit is contained in:
2025-11-24 19:41:41 -08:00
parent fa240b040f
commit 5ab5698fe2
4 changed files with 25 additions and 30 deletions

View File

@@ -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") 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" 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 "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." 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. # We allow the deployment to continue, but a manual schema update is required.

View File

@@ -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.' }); 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 // 2. Prepare flyer data for insertion
const flyerData = { const flyerData = {
file_name: originalFileName, file_name: originalFileName,
image_url: `/assets/${req.file.filename}`, image_url: `/assets/${req.file.filename}`,
checksum: checksum, checksum: checksum,
// Pass the store_name directly to the DB function.
store_name: extractedData.store_name, store_name: extractedData.store_name,
store_id: storeId, // Use the resolved store_id
valid_from: extractedData.valid_from, valid_from: extractedData.valid_from,
valid_to: extractedData.valid_to, valid_to: extractedData.valid_to,
store_address: extractedData.store_address, store_address: extractedData.store_address,

View File

@@ -7,13 +7,15 @@
import type { GroundingChunk } from "@google/genai"; import type { GroundingChunk } from "@google/genai";
import type { FlyerItem, MasterGroceryItem, Store, ExtractedCoreData, ExtractedLogoData } from "../types"; import type { FlyerItem, MasterGroceryItem, Store, ExtractedCoreData, ExtractedLogoData } from "../types";
import { logger } from "./logger"; import { logger } from "./logger";
import { apiFetch } from './apiClient'; // Use the authenticated fetch wrapper import { apiFetchWithAuth } from './apiClient';
export const isImageAFlyer = async (imageFile: File): Promise<boolean> => { export const isImageAFlyer = async (imageFile: File): Promise<boolean> => {
const formData = new FormData(); const formData = new FormData();
formData.append('image', imageFile); 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', method: 'POST',
body: formData, body: formData,
}); });
@@ -25,7 +27,7 @@ export const extractAddressFromImage = async (imageFile: File): Promise<string |
const formData = new FormData(); const formData = new FormData();
formData.append('image', imageFile); formData.append('image', imageFile);
const response = await apiFetch('/ai/extract-address', { const response = await apiFetchWithAuth('/ai/extract-address', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -47,7 +49,7 @@ export const extractCoreDataFromImage = async (imageFiles: File[], masterItems:
// --- END DEBUG LOGGING --- // --- END DEBUG LOGGING ---
// This now calls the real backend endpoint. // This now calls the real backend endpoint.
const response = await apiFetch('/ai/process-flyer', { const response = await apiFetchWithAuth('/ai/process-flyer', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -72,7 +74,7 @@ export const extractLogoFromImage = async (imageFiles: File[]): Promise<Extracte
formData.append('images', file); formData.append('images', file);
}); });
const response = await apiFetch('/ai/extract-logo', { const response = await apiFetchWithAuth('/ai/extract-logo', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -80,7 +82,7 @@ export const extractLogoFromImage = async (imageFiles: File[]): Promise<Extracte
}; };
export const getQuickInsights = async (items: FlyerItem[]): Promise<string> => { export const getQuickInsights = async (items: FlyerItem[]): Promise<string> => {
const response = await apiFetch('/ai/quick-insights', { const response = await apiFetchWithAuth('/ai/quick-insights', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }), body: JSON.stringify({ items }),
@@ -90,7 +92,7 @@ export const getQuickInsights = async (items: FlyerItem[]): Promise<string> => {
}; };
export const getDeepDiveAnalysis = async (items: FlyerItem[]): Promise<string> => { export const getDeepDiveAnalysis = async (items: FlyerItem[]): Promise<string> => {
const response = await apiFetch('/ai/deep-dive', { const response = await apiFetchWithAuth('/ai/deep-dive', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }), body: JSON.stringify({ items }),
@@ -100,7 +102,7 @@ export const getDeepDiveAnalysis = async (items: FlyerItem[]): Promise<string> =
}; };
export const searchWeb = async (items: FlyerItem[]): Promise<{text: string; sources: GroundingChunk[]}> => { 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }), 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; }[]}> => { 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 }); logger.debug("Stub: planTripWithMaps called with location:", { userLocation });
const response = await apiFetch('/ai/plan-trip', { const response = await apiFetchWithAuth('/ai/plan-trip', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, store, userLocation }), 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<string> => { export const generateImageFromText = async (prompt: string): Promise<string> => {
logger.debug("Stub: generateImageFromText called with prompt:", { prompt }); logger.debug("Stub: generateImageFromText called with prompt:", { prompt });
const response = await apiFetch('/ai/generate-image', { const response = await apiFetchWithAuth('/ai/generate-image', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }), body: JSON.stringify({ prompt }),
@@ -152,7 +154,7 @@ export const generateImageFromText = async (prompt: string): Promise<string> =>
*/ */
export const generateSpeechFromText = async (text: string): Promise<string> => { export const generateSpeechFromText = async (text: string): Promise<string> => {
logger.debug("Stub: generateSpeechFromText called with text:", { text }); logger.debug("Stub: generateSpeechFromText called with text:", { text });
const response = await apiFetch('/ai/generate-speech', { const response = await apiFetchWithAuth('/ai/generate-speech', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }), body: JSON.stringify({ text }),

View File

@@ -126,7 +126,7 @@ export async function findFlyerByChecksum(checksum: string): Promise<Flyer | und
*/ */
// prettier-ignore // prettier-ignore
export async function createFlyerAndItems( export async function createFlyerAndItems(
flyerData: Omit<Flyer, 'flyer_id' | 'created_at' | 'store'>, flyerData: Omit<Flyer, 'flyer_id' | 'created_at' | 'store' | 'store_id'> & { store_name: string },
items: Omit<FlyerItem, 'flyer_item_id' | 'flyer_id' | 'created_at'>[] items: Omit<FlyerItem, 'flyer_item_id' | 'flyer_id' | 'created_at'>[]
): Promise<Flyer> { ): Promise<Flyer> {
const client = await getPool().connect(); const client = await getPool().connect();
@@ -135,12 +135,14 @@ export async function createFlyerAndItems(
await client.query('BEGIN'); await client.query('BEGIN');
logger.debug('[DB createFlyerAndItems] BEGIN transaction successful.'); logger.debug('[DB createFlyerAndItems] BEGIN transaction successful.');
// Find or create the store // Find or create the store to get its ID. This logic is now self-contained.
// The store_id is now expected to be passed in the flyerData object, let storeId: number;
// as it's resolved in the route handler. const storeRes = await client.query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [flyerData.store_name]);
const storeId = flyerData.store_id; if (storeRes.rows.length > 0) {
if (!storeId) { storeId = storeRes.rows[0].store_id;
throw new Error("store_id is required to create a flyer."); } 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 // Create the flyer record