diff --git a/backend/src/index.ts b/backend/src/index.ts index f06f119..d3c6cf5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -168,6 +168,10 @@ async function runMigrations() { IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'products' AND column_name = 'anchor_price') THEN ALTER TABLE products ADD COLUMN anchor_price DECIMAL(10,2); END IF; + -- Per-product AI verification disable flag + 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; END $$; `); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index f8346b1..f69a3a3 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -313,6 +313,7 @@ export interface Product { price_drop_threshold: number | null; target_price: number | null; notify_back_in_stock: boolean; + ai_verification_disabled: boolean; created_at: Date; } @@ -476,6 +477,7 @@ export const productQueries = { price_drop_threshold?: number | null; target_price?: number | null; notify_back_in_stock?: boolean; + ai_verification_disabled?: boolean; } ): Promise => { const fields: string[] = []; @@ -502,6 +504,10 @@ export const productQueries = { fields.push(`notify_back_in_stock = $${paramIndex++}`); values.push(updates.notify_back_in_stock); } + if (updates.ai_verification_disabled !== undefined) { + fields.push(`ai_verification_disabled = $${paramIndex++}`); + values.push(updates.ai_verification_disabled); + } if (fields.length === 0) return null; @@ -581,6 +587,14 @@ export const productQueries = { ); return result.rows[0]?.anchor_price ? parseFloat(result.rows[0].anchor_price) : null; }, + + isAiVerificationDisabled: async (id: number): Promise => { + const result = await pool.query( + 'SELECT ai_verification_disabled FROM products WHERE id = $1', + [id] + ); + return result.rows[0]?.ai_verification_disabled === true; + }, }; // Price History types and queries diff --git a/backend/src/routes/products.ts b/backend/src/routes/products.ts index 8d1c6de..542f532 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 } = req.body; + const { name, refresh_interval, price_drop_threshold, target_price, notify_back_in_stock, ai_verification_disabled } = req.body; const product = await productQueries.update(productId, userId, { name, @@ -227,6 +227,7 @@ router.put('/:id', async (req: AuthRequest, res: Response) => { price_drop_threshold, target_price, notify_back_in_stock, + ai_verification_disabled, }); if (!product) { diff --git a/backend/src/services/scheduler.ts b/backend/src/services/scheduler.ts index 1ca4af0..18cbcb7 100644 --- a/backend/src/services/scheduler.ts +++ b/backend/src/services/scheduler.ts @@ -29,14 +29,18 @@ async function checkPrices(): Promise { // Get anchor price for variant products (the price the user confirmed) const anchorPrice = await productQueries.getAnchorPrice(product.id); - console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}`); + // 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}`); // Use voting scraper with preferred method and anchor price if available const scrapedData = await scrapeProductWithVoting( product.url, product.user_id, preferredMethod as ExtractionMethod | undefined, - anchorPrice || undefined + anchorPrice || undefined, + skipAiVerification ); 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 d9de0b7..a96b43e 100644 --- a/backend/src/services/scraper.ts +++ b/backend/src/services/scraper.ts @@ -1355,12 +1355,14 @@ export async function scrapeProduct(url: string, userId?: number): Promise { const result: ScrapedProductWithCandidates = { name: null, @@ -1637,10 +1639,12 @@ export async function scrapeProductWithVoting( } // If we have a price but AI is available, verify it - // SKIP verification if we have multiple candidates - let user choose from modal instead + // SKIP verification if: + // - User disabled AI verification for this product + // - We have multiple candidates (let user choose from modal instead) // This prevents AI from "correcting" valid alternative prices (e.g., other sellers on Amazon) const hasMultipleCandidates = allCandidates.length > 1; - if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates) { + if (result.price && userId && html && !result.aiStatus && !hasMultipleCandidates && !skipAiVerification) { try { const { tryAIVerification } = await import('./ai-extractor'); const verifyResult = await tryAIVerification( diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 75c150e..1ec4122 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -65,6 +65,7 @@ export interface Product { price_drop_threshold: number | null; target_price: number | null; notify_back_in_stock: boolean; + ai_verification_disabled: boolean; created_at: string; current_price: number | null; currency: string | null; @@ -131,6 +132,7 @@ export const productsApi = { price_drop_threshold?: number | null; target_price?: number | null; notify_back_in_stock?: boolean; + ai_verification_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 e5bbd5e..c5fc3cb 100644 --- a/frontend/src/pages/ProductDetail.tsx +++ b/frontend/src/pages/ProductDetail.tsx @@ -30,6 +30,7 @@ export default function ProductDetail() { const [priceDropThreshold, setPriceDropThreshold] = useState(''); const [targetPrice, setTargetPrice] = useState(''); const [notifyBackInStock, setNotifyBackInStock] = useState(false); + const [aiVerificationDisabled, setAiVerificationDisabled] = useState(false); const REFRESH_INTERVALS = [ { value: 300, label: '5 minutes' }, @@ -62,6 +63,7 @@ export default function ProductDetail() { setTargetPrice(productRes.data.target_price.toString()); } setNotifyBackInStock(productRes.data.notify_back_in_stock || false); + setAiVerificationDisabled(productRes.data.ai_verification_disabled || false); } catch { setError('Failed to load product details'); } finally { @@ -139,12 +141,14 @@ export default function ProductDetail() { price_drop_threshold: threshold, target_price: target, notify_back_in_stock: notifyBackInStock, + ai_verification_disabled: aiVerificationDisabled, }); setProduct({ ...product, price_drop_threshold: threshold, target_price: target, notify_back_in_stock: notifyBackInStock, + ai_verification_disabled: aiVerificationDisabled, }); showToast('Notification settings saved'); } catch { @@ -763,6 +767,114 @@ export default function ProductDetail() { )} + + + +
+
+ ⚙️ +

Advanced Settings

+
+

+ Fine-tune how price extraction works for this product. +

+ + + +
+ +
+
); }