additional background job work
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m19s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user