From b9d8d15e6828a252f9347ac600a58693a7198cbb Mon Sep 17 00:00:00 2001 From: clucraft Date: Sat, 24 Jan 2026 20:32:25 -0500 Subject: [PATCH] Add per-product AI verification disable option Users can now disable AI verification for individual products that AI is having trouble with (e.g., Amazon products where AI keeps picking the main buy box price instead of "other sellers"). Changes: - Add ai_verification_disabled column to products table - Add toggle in product detail page under "Advanced Settings" - Pass skip flag to scrapeProductWithVoting - Skip AI verification when flag is set Co-Authored-By: Claude Opus 4.5 --- backend/src/index.ts | 4 + backend/src/models/index.ts | 14 ++++ backend/src/routes/products.ts | 3 +- backend/src/services/scheduler.ts | 8 +- backend/src/services/scraper.ts | 10 ++- frontend/src/api/client.ts | 2 + frontend/src/pages/ProductDetail.tsx | 112 +++++++++++++++++++++++++++ 7 files changed, 147 insertions(+), 6 deletions(-) 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. +

+ + + +
+ +
+
); }