Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728b1a20d3 | ||
| f248f7cbd0 | |||
|
|
0ad9bb16c2 | ||
| 510787bc5b | |||
|
|
9f696e7676 |
@@ -185,7 +185,17 @@ jobs:
|
|||||||
- name: Show PM2 Environment for Production
|
- name: Show PM2 Environment for Production
|
||||||
run: |
|
run: |
|
||||||
echo "--- Displaying recent PM2 logs for flyer-crawler-api ---"
|
echo "--- Displaying recent PM2 logs for flyer-crawler-api ---"
|
||||||
sleep 5
|
sleep 5 # Wait a few seconds for the app to start and log its output.
|
||||||
pm2 describe flyer-crawler-api || echo "Could not find production pm2 process."
|
|
||||||
pm2 logs flyer-crawler-api --lines 20 --nostream || echo "Could not find production pm2 process."
|
# Resolve the PM2 ID dynamically to ensure we target the correct process
|
||||||
pm2 env flyer-crawler-api || echo "Could not find production pm2 process."
|
PM2_ID=$(pm2 jlist | node -e "try { const list = JSON.parse(require('fs').readFileSync(0, 'utf-8')); const app = list.find(p => p.name === 'flyer-crawler-api'); console.log(app ? app.pm2_env.pm_id : ''); } catch(e) { console.log(''); }")
|
||||||
|
|
||||||
|
if [ -n "$PM2_ID" ]; then
|
||||||
|
echo "Found process ID: $PM2_ID"
|
||||||
|
pm2 describe "$PM2_ID" || echo "Failed to describe process $PM2_ID"
|
||||||
|
pm2 logs "$PM2_ID" --lines 20 --nostream || echo "Failed to get logs for $PM2_ID"
|
||||||
|
pm2 env "$PM2_ID" || echo "Failed to get env for $PM2_ID"
|
||||||
|
else
|
||||||
|
echo "Could not find process 'flyer-crawler-api' in pm2 list."
|
||||||
|
pm2 list # Fallback to listing everything to help debug
|
||||||
|
fi
|
||||||
|
|||||||
@@ -461,7 +461,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "--- Displaying recent PM2 logs for flyer-crawler-api-test ---"
|
echo "--- Displaying recent PM2 logs for flyer-crawler-api-test ---"
|
||||||
# After a reload, the server restarts. We'll show the last 20 lines of the log to see the startup messages.
|
# After a reload, the server restarts. We'll show the last 20 lines of the log to see the startup messages.
|
||||||
sleep 5 # Wait a few seconds for the app to start and log its output.
|
sleep 5
|
||||||
pm2 describe flyer-crawler-api-test || echo "Could not find test pm2 process."
|
|
||||||
pm2 logs flyer-crawler-api-test --lines 20 --nostream || echo "Could not find test pm2 process."
|
# Resolve the PM2 ID dynamically to ensure we target the correct process
|
||||||
pm2 env flyer-crawler-api-test || echo "Could not find test pm2 process."
|
PM2_ID=$(pm2 jlist | node -e "try { const list = JSON.parse(require('fs').readFileSync(0, 'utf-8')); const app = list.find(p => p.name === 'flyer-crawler-api-test'); console.log(app ? app.pm2_env.pm_id : ''); } catch(e) { console.log(''); }")
|
||||||
|
|
||||||
|
if [ -n "$PM2_ID" ]; then
|
||||||
|
echo "Found process ID: $PM2_ID"
|
||||||
|
pm2 describe "$PM2_ID" || echo "Failed to describe process $PM2_ID"
|
||||||
|
pm2 logs "$PM2_ID" --lines 20 --nostream || echo "Failed to get logs for $PM2_ID"
|
||||||
|
pm2 env "$PM2_ID" || echo "Failed to get env for $PM2_ID"
|
||||||
|
else
|
||||||
|
echo "Could not find process 'flyer-crawler-api-test' in pm2 list."
|
||||||
|
pm2 list # Fallback to listing everything to help debug
|
||||||
|
fi
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
// --- API Server ---
|
// --- API Server ---
|
||||||
name: 'flyer-crawler-api',
|
name: 'flyer-crawler-api',
|
||||||
|
// Note: The process names below are referenced in .gitea/workflows/ for status checks.
|
||||||
script: './node_modules/.bin/tsx',
|
script: './node_modules/.bin/tsx',
|
||||||
args: 'server.ts',
|
args: 'server.ts',
|
||||||
max_memory_restart: '500M',
|
max_memory_restart: '500M',
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.4.6",
|
"version": "0.5.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.4.6",
|
"version": "0.5.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^6.14.2",
|
"@bull-board/api": "^6.14.2",
|
||||||
"@bull-board/express": "^6.14.2",
|
"@bull-board/express": "^6.14.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.6",
|
"version": "0.5.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function useApi<T, TArgs extends unknown[]>(
|
|||||||
const [isRefetching, setIsRefetching] = useState<boolean>(false);
|
const [isRefetching, setIsRefetching] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
const hasBeenExecuted = useRef(false);
|
const hasBeenExecuted = useRef(false);
|
||||||
|
const lastErrorMessageRef = useRef<string | null>(null);
|
||||||
const abortControllerRef = useRef<AbortController>(new AbortController());
|
const abortControllerRef = useRef<AbortController>(new AbortController());
|
||||||
|
|
||||||
// This effect ensures that when the component using the hook unmounts,
|
// This effect ensures that when the component using the hook unmounts,
|
||||||
@@ -52,6 +53,7 @@ export function useApi<T, TArgs extends unknown[]>(
|
|||||||
async (...args: TArgs): Promise<T | null> => {
|
async (...args: TArgs): Promise<T | null> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
lastErrorMessageRef.current = null;
|
||||||
if (hasBeenExecuted.current) {
|
if (hasBeenExecuted.current) {
|
||||||
setIsRefetching(true);
|
setIsRefetching(true);
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,13 @@ export function useApi<T, TArgs extends unknown[]>(
|
|||||||
error: err.message,
|
error: err.message,
|
||||||
functionName: apiFunction.name,
|
functionName: apiFunction.name,
|
||||||
});
|
});
|
||||||
setError(err);
|
// Only set a new error object if the message is different from the last one.
|
||||||
|
// This prevents creating new object references for the same error (e.g. repeated timeouts)
|
||||||
|
// and helps break infinite loops in components that depend on the `error` object.
|
||||||
|
if (err.message !== lastErrorMessageRef.current) {
|
||||||
|
setError(err);
|
||||||
|
lastErrorMessageRef.current = err.message;
|
||||||
|
}
|
||||||
notifyError(err.message); // Optionally notify the user automatically.
|
notifyError(err.message); // Optionally notify the user automatically.
|
||||||
return null; // Return null on failure.
|
return null; // Return null on failure.
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function useInfiniteQuery<T>(
|
|||||||
|
|
||||||
// Use a ref to store the cursor for the next page.
|
// Use a ref to store the cursor for the next page.
|
||||||
const nextCursorRef = useRef<number | string | null | undefined>(initialCursor);
|
const nextCursorRef = useRef<number | string | null | undefined>(initialCursor);
|
||||||
|
const lastErrorMessageRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const fetchPage = useCallback(
|
const fetchPage = useCallback(
|
||||||
async (cursor?: number | string | null) => {
|
async (cursor?: number | string | null) => {
|
||||||
@@ -59,6 +60,7 @@ export function useInfiniteQuery<T>(
|
|||||||
setIsFetchingNextPage(true);
|
setIsFetchingNextPage(true);
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
lastErrorMessageRef.current = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFunction(cursor);
|
const response = await apiFunction(cursor);
|
||||||
@@ -99,7 +101,10 @@ export function useInfiniteQuery<T>(
|
|||||||
error: err.message,
|
error: err.message,
|
||||||
functionName: apiFunction.name,
|
functionName: apiFunction.name,
|
||||||
});
|
});
|
||||||
setError(err);
|
if (err.message !== lastErrorMessageRef.current) {
|
||||||
|
setError(err);
|
||||||
|
lastErrorMessageRef.current = err.message;
|
||||||
|
}
|
||||||
notifyError(err.message);
|
notifyError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -125,6 +130,7 @@ export function useInfiniteQuery<T>(
|
|||||||
// Function to be called by the UI to refetch the entire query from the beginning.
|
// Function to be called by the UI to refetch the entire query from the beginning.
|
||||||
const refetch = useCallback(() => {
|
const refetch = useCallback(() => {
|
||||||
setIsRefetching(true);
|
setIsRefetching(true);
|
||||||
|
lastErrorMessageRef.current = null;
|
||||||
setData([]);
|
setData([]);
|
||||||
fetchPage(initialCursor);
|
fetchPage(initialCursor);
|
||||||
}, [fetchPage, initialCursor]);
|
}, [fetchPage, initialCursor]);
|
||||||
|
|||||||
0
src/routes/personalization.db.ts
Normal file
0
src/routes/personalization.db.ts
Normal file
@@ -84,10 +84,10 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
.send({ items: [{ item: 'test' }] });
|
.send({ items: [{ item: 'test' }] });
|
||||||
const result = response.body;
|
const result = response.body;
|
||||||
// DEBUG: Log response if it fails expectation
|
// DEBUG: Log response if it fails expectation
|
||||||
if (response.status !== 404 || !result.text) {
|
if (response.status !== 200 || !result.text) {
|
||||||
console.log('[DEBUG] POST /api/ai/quick-insights response:', response.status, response.body);
|
console.log('[DEBUG] POST /api/ai/quick-insights response:', response.status, response.body);
|
||||||
}
|
}
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(200);
|
||||||
expect(result.text).toBe('This is a server-generated quick insight: buy the cheap stuff!');
|
expect(result.text).toBe('This is a server-generated quick insight: buy the cheap stuff!');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
.send({ items: [{ item: 'test' }] });
|
.send({ items: [{ item: 'test' }] });
|
||||||
const result = response.body;
|
const result = response.body;
|
||||||
// DEBUG: Log response if it fails expectation
|
// DEBUG: Log response if it fails expectation
|
||||||
if (response.status !== 404 || !result.text) {
|
if (response.status !== 200 || !result.text) {
|
||||||
console.log('[DEBUG] POST /api/ai/deep-dive response:', response.status, response.body);
|
console.log('[DEBUG] POST /api/ai/deep-dive response:', response.status, response.body);
|
||||||
}
|
}
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(200);
|
||||||
expect(result.text).toBe('This is a server-generated deep dive analysis. It is very detailed.');
|
expect(result.text).toBe('This is a server-generated deep dive analysis. It is very detailed.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
.send({ query: 'test query' });
|
.send({ query: 'test query' });
|
||||||
const result = response.body;
|
const result = response.body;
|
||||||
// DEBUG: Log response if it fails expectation
|
// DEBUG: Log response if it fails expectation
|
||||||
if (response.status !== 404 || !result.text) {
|
if (response.status !== 200 || !result.text) {
|
||||||
console.log('[DEBUG] POST /api/ai/search-web response:', response.status, response.body);
|
console.log('[DEBUG] POST /api/ai/search-web response:', response.status, response.body);
|
||||||
}
|
}
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(200);
|
||||||
expect(result).toEqual({ text: 'The web says this is good.', sources: [] });
|
expect(result).toEqual({ text: 'The web says this is good.', sources: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
.post('/api/ai/generate-image')
|
.post('/api/ai/generate-image')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ prompt: 'a test prompt' });
|
.send({ prompt: 'a test prompt' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(501);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
it('POST /api/ai/generate-speech should reject because it is not implemented', async () => {
|
||||||
@@ -178,6 +178,6 @@ describe('AI API Routes Integration Tests', () => {
|
|||||||
.post('/api/ai/generate-speech')
|
.post('/api/ai/generate-speech')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.send({ text: 'a test prompt' });
|
.send({ text: 'a test prompt' });
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(501);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
if (jobStatus?.state === 'failed') {
|
if (jobStatus?.state === 'failed') {
|
||||||
console.error('[DEBUG] Job failed with reason:', jobStatus.failedReason);
|
console.error('[DEBUG] Job failed with reason:', jobStatus.failedReason);
|
||||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||||
|
console.error('[DEBUG] Full Job Status:', JSON.stringify(jobStatus, null, 2));
|
||||||
}
|
}
|
||||||
expect(jobStatus?.state).toBe('completed');
|
expect(jobStatus?.state).toBe('completed');
|
||||||
const flyerId = jobStatus?.returnValue?.flyerId;
|
const flyerId = jobStatus?.returnValue?.flyerId;
|
||||||
|
|||||||
@@ -42,9 +42,23 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
// DEBUG: Verify user existence in DB
|
// DEBUG: Verify user existence in DB
|
||||||
console.log(`[DEBUG] createAndLoginUser returned user ID: ${testUser.user.user_id}`);
|
console.log(`[DEBUG] createAndLoginUser returned user ID: ${testUser.user.user_id}`);
|
||||||
const userCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]);
|
const userCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]);
|
||||||
console.log(`[DEBUG] DB check for user found ${userCheck.rowCount} rows.`);
|
console.log(`[DEBUG] DB check for user found ${userCheck.rowCount ?? 0} rows.`);
|
||||||
if (userCheck.rowCount === 0) {
|
if (!userCheck.rowCount) {
|
||||||
console.error(`[DEBUG] CRITICAL: User ${testUser.user.user_id} does not exist in public.users table!`);
|
console.error(`[DEBUG] CRITICAL: User ${testUser.user.user_id} does not exist in public.users table! Attempting to wait...`);
|
||||||
|
// Wait loop to ensure user persistence if there's a race condition
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
const retryCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]);
|
||||||
|
if (retryCheck.rowCount && retryCheck.rowCount > 0) {
|
||||||
|
console.log(`[DEBUG] User found after retry ${i + 1}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Final check before proceeding to avoid FK error
|
||||||
|
const finalCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]);
|
||||||
|
if (!finalCheck.rowCount) {
|
||||||
|
throw new Error(`User ${testUser.user.user_id} failed to persist in DB. Cannot continue test.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a recipe
|
// Create a recipe
|
||||||
|
|||||||
Reference in New Issue
Block a user