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() {
+
+