working ! testing !
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 2m31s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 2m31s
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user