some more re-org + fixes
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 1m1s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 1m1s
This commit is contained in:
@@ -5,7 +5,7 @@ import listEndpoints from 'express-list-endpoints';
|
||||
import { getPool } from './src/services/db/connection';
|
||||
|
||||
import passport from './src/routes/passport';
|
||||
import { logger } from './src/services/logger';
|
||||
import { logger } from './src/services/logger.server';
|
||||
|
||||
// Import routers
|
||||
import authRouter from './src/routes/auth';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isAdmin } from './passport';
|
||||
import multer from 'multer';
|
||||
|
||||
import * as db from '../services/db';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import passport from './passport';
|
||||
import { optionalAuth } from './passport';
|
||||
import * as db from '../services/db';
|
||||
import * as aiService from '../services/aiService.server'; // Correctly import server-side AI service
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { UserProfile } from '../types';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -9,8 +9,8 @@ import rateLimit from 'express-rate-limit';
|
||||
import passport from './passport';
|
||||
import * as db from '../services/db';
|
||||
import { getPool } from '../services/db/connection';
|
||||
import { logger } from '../services/logger';
|
||||
import { sendPasswordResetEmail } from '../services/emailService';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { sendPasswordResetEmail } from '../services/emailService.server';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -230,7 +230,7 @@ router.post('/reset-password', resetPasswordLimiter, async (req: Request, res: R
|
||||
action: 'password_reset',
|
||||
displayText: `User ID ${tokenRecord.user_id} has reset their password.`,
|
||||
icon: 'key',
|
||||
details: { source_ip: req.ip }
|
||||
details: { source_ip: req.ip ?? null }
|
||||
});
|
||||
|
||||
res.status(200).json({ message: 'Password has been reset successfully.' });
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import * as db from '../services/db';
|
||||
import { logger } from '../services/logger';
|
||||
//import { sendWelcomeEmail } from '../services/emailService';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { UserProfile } from '../types';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_jwt_key_change_this';
|
||||
@@ -70,7 +69,7 @@ passport.use(new LocalStrategy(
|
||||
action: 'login_failed_password',
|
||||
displayText: `Failed login attempt for user ${user.email}.`,
|
||||
icon: 'shield-alert',
|
||||
details: { source_ip: req.ip }
|
||||
details: { source_ip: req.ip ?? null }
|
||||
});
|
||||
return done(null, false, { message: 'Incorrect email or password.' });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/public.ts
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as db from '../services/db';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/system.ts
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/user.integration.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { getPool } from '../services/db/connection';
|
||||
import type { User } from '../types';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as bcrypt from 'bcrypt';
|
||||
|
||||
import * as db from '../services/db';
|
||||
import * as aiService from '../services/aiService.server';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { ReceiptItem, ShoppingListItem } from '../types';
|
||||
|
||||
const router = Router();
|
||||
@@ -320,7 +320,7 @@ router.post('/receipts/upload', upload.single('receiptImage'), async (req: Reque
|
||||
newReceipt = await db.createReceipt(user.user_id, receiptImageUrl);
|
||||
|
||||
try {
|
||||
logger.info(`Starting AI receipt processing for receipt ID: ${newReceipt.id}`);
|
||||
logger.info(`Starting AI receipt processing for receipt ID: ${newReceipt.receipt_id}`);
|
||||
// The AI service might not return quantity, so we ensure it's added.
|
||||
const extractedItemsFromAI: { raw_item_description: string; price_paid_cents: number; quantity?: number }[] =
|
||||
await aiService.extractItemsFromReceiptImage(req.file.path, req.file.mimetype);
|
||||
@@ -332,12 +332,12 @@ router.post('/receipts/upload', upload.single('receiptImage'), async (req: Reque
|
||||
quantity: item.quantity || 1, // Default quantity to 1 if not provided
|
||||
}));
|
||||
|
||||
await db.processReceiptItems(newReceipt.id, itemsToProcess);
|
||||
logger.info(`Completed AI receipt processing for receipt ID: ${newReceipt.id}. Found ${itemsToProcess.length} items.`);
|
||||
await db.processReceiptItems(newReceipt.receipt_id, itemsToProcess);
|
||||
logger.info(`Completed AI receipt processing for receipt ID: ${newReceipt.receipt_id}. Found ${itemsToProcess.length} items.`);
|
||||
|
||||
} catch (processingError) {
|
||||
logger.error(`Receipt processing failed for receipt ID: ${newReceipt.id}.`, { error: processingError });
|
||||
await db.updateReceiptStatus(newReceipt.id, 'failed');
|
||||
logger.error(`Receipt processing failed for receipt ID: ${newReceipt.receipt_id}.`, { error: processingError });
|
||||
await db.updateReceiptStatus(newReceipt.receipt_id, 'failed');
|
||||
throw processingError;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import fs from 'fs/promises';
|
||||
import { logger } from './logger';
|
||||
import { logger } from './logger.server';
|
||||
import type { FlyerItem, MasterGroceryItem } from '../types';
|
||||
|
||||
// Use the secure, server-side API key.
|
||||
@@ -189,7 +189,7 @@ export const planTripWithMaps = async (items: FlyerItem[], store: { name: string
|
||||
uri: chunk.web?.uri || '',
|
||||
title: chunk.web?.title || 'Untitled'
|
||||
}));
|
||||
return { text: response.text, sources };
|
||||
return { text: response.text ?? '', sources };
|
||||
} catch (apiError) {
|
||||
logger.error("Google GenAI API call failed in planTripWithMaps:", { error: apiError });
|
||||
throw apiError;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { logger } from './logger';
|
||||
import { logger } from './logger.server';
|
||||
|
||||
let transporter: nodemailer.Transporter;
|
||||
|
||||
20
src/services/logger.client.ts
Normal file
20
src/services/logger.client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* A simple, client-side logger service that wraps the console.
|
||||
* This version is guaranteed to be safe for browser environments as it
|
||||
* does not reference any Node.js-specific globals like `process`.
|
||||
*/
|
||||
|
||||
type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
|
||||
|
||||
const log = <T extends unknown[]>(level: LogLevel, message: string, ...args: T) => {
|
||||
const logMessage = `[${level}] ${message}`;
|
||||
console.log(logMessage, ...args);
|
||||
};
|
||||
|
||||
// Export the logger object for use throughout the client-side application.
|
||||
export const logger = {
|
||||
info: <T extends unknown[]>(message: string, ...args: T) => log('INFO', message, ...args),
|
||||
warn: <T extends unknown[]>(message: string, ...args: T) => log('WARN', message, ...args),
|
||||
error: <T extends unknown[]>(message: string, ...args: T) => log('ERROR', message, ...args),
|
||||
debug: <T extends unknown[]>(message: string, ...args: T) => log('DEBUG', message, ...args),
|
||||
};
|
||||
46
src/services/logger.server.ts
Normal file
46
src/services/logger.server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* SERVER-SIDE LOGGER
|
||||
* A logger service that includes server-specific details like process ID.
|
||||
* This file should only be imported in Node.js environments.
|
||||
* such as adding timestamps, log levels, or sending logs to a remote service.
|
||||
*/
|
||||
|
||||
const getTimestamp = () => new Date().toISOString();
|
||||
|
||||
type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
|
||||
|
||||
/**
|
||||
* The core logging function. It uses a generic rest parameter `<T extends any[]>`
|
||||
* to safely accept any number of arguments of any type, just like the native console
|
||||
* methods. Using `unknown[]` is more type-safe than `any[]` and satisfies the linter.
|
||||
*/
|
||||
const log = <T extends unknown[]>(level: LogLevel, message: string, ...args: T) => {
|
||||
const pid = process.pid;
|
||||
const timestamp = getTimestamp();
|
||||
// We construct the log message with a timestamp, PID, and level for better context.
|
||||
const logMessage = `[${timestamp}] [PID:${pid}] [${level}] ${message}`;
|
||||
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
console.log(logMessage, ...args);
|
||||
break;
|
||||
case 'WARN':
|
||||
console.warn(logMessage, ...args);
|
||||
break;
|
||||
case 'ERROR':
|
||||
console.error(logMessage, ...args);
|
||||
break;
|
||||
case 'DEBUG':
|
||||
// For now, we can show debug logs in development. This could be controlled by an environment variable.
|
||||
console.debug(logMessage, ...args);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Export the logger object for use throughout the application.
|
||||
export const logger = {
|
||||
info: <T extends unknown[]>(message: string, ...args: T) => log('INFO', message, ...args),
|
||||
warn: <T extends unknown[]>(message: string, ...args: T) => log('WARN', message, ...args),
|
||||
error: <T extends unknown[]>(message: string, ...args: T) => log('ERROR', message, ...args),
|
||||
debug: <T extends unknown[]>(message: string, ...args: T) => log('DEBUG', message, ...args),
|
||||
};
|
||||
@@ -14,10 +14,14 @@ type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
|
||||
* methods. Using `unknown[]` is more type-safe than `any[]` and satisfies the linter.
|
||||
*/
|
||||
const log = <T extends unknown[]>(level: LogLevel, message: string, ...args: T) => {
|
||||
const pid = process.pid;
|
||||
// Check if `process` is available (Node.js) vs. browser environment.
|
||||
// This makes the logger "isomorphic" and prevents runtime errors on the client.
|
||||
const envIdentifier = typeof process !== 'undefined' && process.pid
|
||||
? `PID:${process.pid}`
|
||||
: 'BROWSER';
|
||||
const timestamp = getTimestamp();
|
||||
// We construct the log message with a timestamp, PID, and level for better context.
|
||||
const logMessage = `[${timestamp}] [PID:${pid}] [${level}] ${message}`;
|
||||
const logMessage = `[${timestamp}] [${envIdentifier}] [${level}] ${message}`;
|
||||
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
|
||||
@@ -19,17 +19,20 @@ export default defineConfig({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(process.cwd(), './src'),
|
||||
// Use __dirname for a more robust path resolution
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
// This alias ensures that any import of 'services/logger' is resolved
|
||||
// to the browser-safe client version during the Vite build process.
|
||||
// Server-side code should explicitly import 'services/logger.server'.
|
||||
'services/logger': path.resolve(__dirname, './src/services/logger.client.ts'),
|
||||
},
|
||||
},
|
||||
|
||||
// Vitest-specific configuration for the 'unit' test project.
|
||||
test: {
|
||||
// DEBUGGING LOG: return 'true' to allow the log, 'false' to suppress it.
|
||||
// We want to SEE debug logs.
|
||||
onConsoleLog(log) { if (log.includes('[DEBUG]')) return true; },
|
||||
// The name for this project is defined by the filename in the workspace config.
|
||||
name: 'unit',
|
||||
// By default, Vitest does not suppress console logs.
|
||||
// The onConsoleLog hook is only needed if you want to conditionally filter specific logs.
|
||||
// Keeping the default behavior is often safer to avoid missing important warnings.
|
||||
environment: 'jsdom',
|
||||
globalSetup: './src/tests/setup/global-setup.ts',
|
||||
setupFiles: ['./src/tests/setup/unit-setup.ts'],
|
||||
@@ -49,9 +52,15 @@ export default defineConfig({
|
||||
reportsDirectory: './.coverage/unit',
|
||||
clean: true,
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
// Refine exclusions to be more comprehensive
|
||||
exclude: [
|
||||
'src/main.tsx', 'src/vite-env.d.ts', 'src/types.ts', 'src/vitest.setup.ts',
|
||||
'src/**/*.test.{ts,tsx}', 'src/components/icons', 'src/services/logger.ts', 'src/services/notificationService.ts'
|
||||
'src/main.tsx',
|
||||
'src/types.ts',
|
||||
'src/tests/**', // Exclude all test setup and helper files
|
||||
'src/**/*.test.{ts,tsx}', // Exclude test files themselves
|
||||
'src/**/*.stories.{ts,tsx}', // Exclude Storybook stories
|
||||
'src/**/*.d.ts', // Exclude type definition files
|
||||
'src/components/icons/**', // Exclude icon components if they are simple wrappers
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user