additional background job work
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s

This commit is contained in:
2025-12-03 15:32:09 -08:00
parent 383e8e3d25
commit 893ae6da53
7 changed files with 129 additions and 6 deletions

View File

@@ -1,10 +1,12 @@
// src/pages/admin/components/SystemCheck.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { loginUser, pingBackend, checkDbSchema, checkStorage, checkDbPoolHealth, checkPm2Status, checkRedisHealth } from '../../../services/apiClient';
import toast from 'react-hot-toast';
import { loginUser, pingBackend, checkDbSchema, checkStorage, checkDbPoolHealth, checkPm2Status, checkRedisHealth, triggerFailingJob } from '../../../services/apiClient';
import { ShieldCheckIcon } from '../../../components/icons/ShieldCheckIcon';
import { LoadingSpinner } from '../../../components/LoadingSpinner';
import { CheckCircleIcon } from '../../../components/icons/CheckCircleIcon';
import { XCircleIcon } from '../../../components/icons/XCircleIcon';
import { BeakerIcon } from 'lucide-react';
type TestStatus = 'idle' | 'running' | 'pass' | 'fail';
@@ -44,6 +46,7 @@ export const SystemCheck: React.FC = () => {
const [isRunning, setIsRunning] = useState(false);
const [hasRunAutoTest, setHasRunAutoTest] = useState(false);
const [elapsedTime, setElapsedTime] = useState<number | null>(null);
const [isTriggeringJob, setIsTriggeringJob] = useState(false);
const updateCheckStatus = useCallback((id: CheckID, status: TestStatus, message: string) => {
setChecks(prev => prev.map(c => c.id === id ? { ...c, status, message } : c));
@@ -209,6 +212,23 @@ export const SystemCheck: React.FC = () => {
}
}, [checkApiKey, checkBackendConnection, checkPm2Process, checkDatabasePool, checkRedisConnection, checkDatabaseSchema, checkStorageDirectory, checkSeededUsers, updateCheckStatus]);
const handleTriggerFailingJob = async () => {
setIsTriggeringJob(true);
try {
const response = await triggerFailingJob();
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to trigger job.');
}
const data = await response.json();
toast.success(data.message);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'An unknown error occurred.');
} finally {
setIsTriggeringJob(false);
}
};
useEffect(() => {
if (!hasRunAutoTest) {
setHasRunAutoTest(true);
@@ -271,6 +291,35 @@ export const SystemCheck: React.FC = () => {
)}
</button>
</div>
{/* New section for integration confirmation */}
<div className="mt-8 border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex items-center mb-3">
<BeakerIcon className="w-6 h-6 mr-2 text-brand-primary" />
Confirm Integration: Job Queue Retries
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
<p>Use this to test the background job queue's retry mechanism and the Bull Board UI.</p>
<ol className="list-decimal list-inside space-y-1 pl-2">
<li>Click the button below to enqueue a job that is designed to fail.</li>
<li>Navigate to the <a href="/api/admin/jobs" target="_blank" rel="noopener noreferrer" className="text-brand-primary hover:underline">Job Queue Dashboard</a>.</li>
<li>Observe the job in the 'analytics-reporting' queue. It will become active, then fail and move to 'delayed' for its first retry.</li>
<li>After the final attempt, it will move to the 'failed' list.</li>
<li>In the 'failed' list, a "Retry" button will appear, allowing you to manually trigger the job again.</li>
</ol>
</div>
<div className="mt-4">
<button
onClick={handleTriggerFailingJob}
disabled={isTriggeringJob}
className="bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-wait text-white font-bold py-2 px-4 rounded-lg transition-colors duration-300 flex items-center justify-center"
>
{isTriggeringJob ? (
<><div className="w-5 h-5 mr-2"><LoadingSpinner /></div> Triggering...</>
) : 'Trigger Failing Job'}
</button>
</div>
</div>
</div>
);
};

View File

@@ -257,4 +257,21 @@ router.post('/flyers/:flyerId/cleanup', async (req: Request, res: Response, next
res.status(202).json({ message: `File cleanup job for flyer ID ${flyerId} has been enqueued.` });
});
/**
* POST /api/admin/trigger/failing-job - Enqueue a test job designed to fail.
* This is for testing the retry mechanism and Bull Board UI.
*/
router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for a failing job received from user: ${adminUser.user_id}`);
try {
// Add a job with a special 'forceFail' flag that the worker will recognize.
const job = await analyticsQueue.add('generate-daily-report', { reportDate: 'FAIL' });
res.status(202).json({ message: `Failing test job has been enqueued successfully. Job ID: ${job.id}` });
} catch (error) {
next(error);
}
});
export default router;

View File

@@ -83,6 +83,18 @@ router.post('/upload-and-process', optionalAuth, uploadToDisk.single('flyerFile'
}
const user = req.user as UserProfile | undefined;
// Construct a user address string from their profile if they are logged in.
let userProfileAddress: string | undefined = undefined;
if (user?.address_line_1) {
userProfileAddress = [
user.address_line_1,
user.address_line_2,
user.city,
user.province_state,
user.postal_code,
user.country
].filter(Boolean).join(', ');
}
// Add job to the queue
const job = await flyerQueue.add('process-flyer', {
@@ -90,6 +102,8 @@ router.post('/upload-and-process', optionalAuth, uploadToDisk.single('flyerFile'
originalFileName: req.file.originalname,
checksum: checksum,
userId: user?.user_id,
submitterIp: req.ip, // Capture the submitter's IP address
userProfileAddress: userProfileAddress, // Pass the user's profile address
});
logger.info(`Enqueued flyer for processing. File: ${req.file.originalname}, Job ID: ${job.id}`);

View File

@@ -105,7 +105,9 @@ type RawFlyerItem = {
*/
export const extractCoreDataFromFlyerImage = async (
imagePaths: { path: string; mimetype: string }[],
masterItems: MasterGroceryItem[]
masterItems: MasterGroceryItem[],
submitterIp?: string,
userProfileAddress?: string
): Promise<{
store_name: string;
valid_from: string | null;
@@ -114,6 +116,13 @@ export const extractCoreDataFromFlyerImage = async (
items: Omit<FlyerItem, 'id' | 'created_at' | 'flyer_id'>[];
}> => {
// 1. Construct the detailed prompt for the AI.
let locationHint = '';
if (userProfileAddress) {
locationHint = `The user who uploaded this flyer has a profile address of "${userProfileAddress}". Use this as a strong hint for the store's location.`;
} else if (submitterIp) {
locationHint = `The user uploaded this flyer from an IP address that suggests a location. Use this as a general hint for the store's region.`;
}
const prompt = `
Analyze the provided flyer image(s). Your task is to extract key information and a list of all sale items.
@@ -121,7 +130,7 @@ export const extractCoreDataFromFlyerImage = async (
- "store_name": The name of the grocery store (e.g., "Walmart", "No Frills").
- "valid_from": The start date of the sale period in YYYY-MM-DD format. If not present, use null.
- "valid_to": The end date of the sale period in YYYY-MM-DD format. If not present, use null.
- "store_address": The physical address of the store if present. If not present, use null.
- "store_address": The physical address of the store if present. If not present, use null. ${locationHint}
Second, extract each individual sale item. For each item, provide:
- "item": The name of the product (e.g., "Coca-Cola Classic").

View File

@@ -680,6 +680,16 @@ export const cleanupFlyerFiles = async (flyerId: number, tokenOverride?: string)
return apiFetch(`/admin/flyers/${flyerId}/cleanup`, { method: 'POST' }, tokenOverride);
};
/**
* Enqueues a test job designed to fail, for testing the Bull Board UI.
* Requires admin privileges.
* @param tokenOverride Optional token for testing.
*/
export const triggerFailingJob = async (tokenOverride?: string): Promise<Response> => {
// This is an admin-only endpoint, so we use apiFetch to include the auth token.
return apiFetch(`/admin/trigger/failing-job`, { method: 'POST' }, tokenOverride);
};
export async function registerUser(
email: string,
password: string,

View File

@@ -73,6 +73,8 @@ interface FlyerJobData {
originalFileName: string;
checksum: string;
userId?: string;
submitterIp?: string;
userProfileAddress?: string;
}
interface EmailJobData {
@@ -100,7 +102,7 @@ interface CleanupJobData {
export const flyerWorker = new Worker<FlyerJobData>(
'flyer-processing',
async (job: Job<FlyerJobData>) => {
const { filePath, originalFileName, checksum, userId } = job.data;
const { filePath, originalFileName, checksum, userId, submitterIp, userProfileAddress } = job.data;
const createdImagePaths: string[] = [];
let jobSucceeded = false;
logger.info(`[Worker] Processing job ${job.id} for file: ${originalFileName}`);
@@ -144,7 +146,12 @@ export const flyerWorker = new Worker<FlyerJobData>(
// 2. Call AI Service
const masterItems = await db.getAllMasterItems(); // Fetch master items for the AI
const extractedData = await aiService.extractCoreDataFromFlyerImage(imagePaths, masterItems);
const extractedData = await aiService.extractCoreDataFromFlyerImage(
imagePaths,
masterItems,
submitterIp,
userProfileAddress
);
logger.info(`[Worker] AI extracted ${extractedData.items.length} items.`);
// 3. Save to Database
@@ -258,6 +265,11 @@ export const analyticsWorker = new Worker<AnalyticsJobData>(
const { reportDate } = job.data;
logger.info(`[AnalyticsWorker] Starting report generation for job ${job.id}`, { reportDate });
try {
// Special case for testing the retry mechanism
if (reportDate === 'FAIL') {
throw new Error('This is a test failure for the analytics job.');
}
// In a real implementation, you would call a database function here.
// For example: await db.generateDailyAnalyticsReport(reportDate);
await new Promise(resolve => setTimeout(resolve, 10000)); // Simulate a 10-second task