diff --git a/backend/src/index.ts b/backend/src/index.ts index 7407c49..892d4e9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -178,6 +178,10 @@ async function runMigrations() { IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_verification_disabled') THEN ALTER TABLE products ADD COLUMN ai_verification_disabled BOOLEAN DEFAULT false; END IF; + -- Per-product AI extraction disable flag + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'ai_extraction_disabled') THEN + ALTER TABLE products ADD COLUMN ai_extraction_disabled BOOLEAN DEFAULT false; + END IF; END $$; `); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 7e96943..c612438 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -326,6 +326,7 @@ export interface Product { target_price: number | null; notify_back_in_stock: boolean; ai_verification_disabled: boolean; + ai_extraction_disabled: boolean; created_at: Date; } @@ -490,6 +491,7 @@ export const productQueries = { target_price?: number | null; notify_back_in_stock?: boolean; ai_verification_disabled?: boolean; + ai_extraction_disabled?: boolean; } ): Promise => { const fields: string[] = []; @@ -520,6 +522,10 @@ export const productQueries = { fields.push(`ai_verification_disabled = $${paramIndex++}`); values.push(updates.ai_verification_disabled); } + if (updates.ai_extraction_disabled !== undefined) { + fields.push(`ai_extraction_disabled = $${paramIndex++}`); + values.push(updates.ai_extraction_disabled); + } if (fields.length === 0) return null; @@ -607,6 +613,14 @@ export const productQueries = { ); return result.rows[0]?.ai_verification_disabled === true; }, + + isAiExtractionDisabled: async (id: number): Promise => { + const result = await pool.query( + 'SELECT ai_extraction_disabled FROM products WHERE id = $1', + [id] + ); + return result.rows[0]?.ai_extraction_disabled === true; + }, }; // Price History types and queries diff --git a/backend/src/routes/products.ts b/backend/src/routes/products.ts index 542f532..f2d4d32 100644 --- a/backend/src/routes/products.ts +++ b/backend/src/routes/products.ts @@ -219,7 +219,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => { return; } - const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled } = req.body; + const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled, ai_extraction_disabled } = req.body; const product = await productQueries.update(productId, userId, { name, @@ -228,6 +228,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => { target_price, notify_back_in_stock, ai_verification_disabled, + ai_extraction_disabled, }); if (!product) { diff --git a/backend/src/services/scheduler.ts b/backend/src/services/scheduler.ts index 18cbcb7..be05777 100644 --- a/backend/src/services/scheduler.ts +++ b/backend/src/services/scheduler.ts @@ -32,7 +32,10 @@ async function checkPrices(): Promise { // Check if AI verification is disabled for this product const skipAiVerification = await productQueries.isAiVerificationDisabled(product.id); - console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAi: ${skipAiVerification}`); + // Check if AI extraction is disabled for this product + const skipAiExtraction = await productQueries.isAiExtractionDisabled(product.id); + + console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}, skipAiVerify: ${skipAiVerification}, skipAiExtract: ${skipAiExtraction}`); // Use voting scraper with preferred method and anchor price if available const scrapedData = await scrapeProductWithVoting( @@ -40,7 +43,8 @@ async function checkPrices(): Promise { product.user_id, preferredMethod as ExtractionMethod | undefined, anchorPrice || undefined, - skipAiVerification + skipAiVerification, + skipAiExtraction ); console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`); diff --git a/backend/src/services/scraper.ts b/backend/src/services/scraper.ts index a96b43e..0ec1c48 100644 --- a/backend/src/services/scraper.ts +++ b/backend/src/services/scraper.ts @@ -1356,13 +1356,15 @@ export async function scrapeProduct(url: string, userId?: number): Promise { const result: ScrapedProductWithCandidates = { name: null, @@ -1608,8 +1610,8 @@ export async function scrapeProductWithVoting( result.selectedMethod = bestCandidate.method; } } else { - // No candidates at all - try pure AI extraction - if (userId && html) { + // No candidates at all - try pure AI extraction (unless disabled for this product) + if (userId && html && !skipAiExtraction) { console.log(`[Voting] No candidates found, trying AI extraction...`); try { const { tryAIExtraction } = await import('./ai-extractor'); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 6658fee..4d7c3ae 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -66,6 +66,7 @@ export interface Product { target_price: number | null; notify_back_in_stock: boolean; ai_verification_disabled: boolean; + ai_extraction_disabled: boolean; created_at: string; current_price: number | null; currency: string | null; @@ -133,6 +134,7 @@ export const productsApi = { target_price?: number | null; notify_back_in_stock?: boolean; ai_verification_disabled?: boolean; + ai_extraction_disabled?: boolean; }) => api.put(`/products/${id}`, data), delete: (id: number) => api.delete(`/products/${id}`), diff --git a/frontend/src/pages/ProductDetail.tsx b/frontend/src/pages/ProductDetail.tsx index c5fc3cb..803b39c 100644 --- a/frontend/src/pages/ProductDetail.tsx +++ b/frontend/src/pages/ProductDetail.tsx @@ -31,6 +31,7 @@ export default function ProductDetail() { const [targetPrice, setTargetPrice] = useState(''); const [notifyBackInStock, setNotifyBackInStock] = useState(false); const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false); + const [aiExtractionDisabled, setAiExtractionDisabled] = useState(false); const REFRESH_INTERVALS = [ { value: 300, label: '5 minutes' }, @@ -64,6 +65,7 @@ export default function ProductDetail() { } setNotifyBackInStock(productRes.data.notify_back_in_stock || false); setAiVerificationDisabled(productRes.data.ai_verification_disabled || false); + setAiExtractionDisabled(productRes.data.ai_extraction_disabled || false); } catch { setError('Failed to load product details'); } finally { @@ -142,6 +144,7 @@ export default function ProductDetail() { target_price: target, notify_back_in_stock: notifyBackInStock, ai_verification_disabled: aiVerificationDisabled, + ai_extraction_disabled: aiExtractionDisabled, }); setProduct({ ...product, @@ -149,6 +152,7 @@ export default function ProductDetail() { target_price: target, notify_back_in_stock: notifyBackInStock, ai_verification_disabled: aiVerificationDisabled, + ai_extraction_disabled: aiExtractionDisabled, }); showToast('Notification settings saved'); } catch { @@ -854,6 +858,18 @@ export default function ProductDetail() {

+ +