From a4d5e95937dcf748282356d68ec1416f7e04f5b8 Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Mon, 24 Nov 2025 14:46:17 -0800 Subject: [PATCH] some more re-org + fixes --- server.ts | 2 +- src/routes/admin.ts | 2 +- src/routes/ai.ts | 2 +- src/routes/auth.ts | 6 +-- src/routes/passport.ts | 5 +- src/routes/public.ts | 2 +- src/routes/system.ts | 2 +- src/routes/user.integration.test.ts | 2 +- src/routes/user.ts | 12 ++--- src/services/aiService.server.ts | 4 +- ...emailService.ts => emailService.server.ts} | 2 +- src/services/logger.client.ts | 20 ++++++++ src/services/logger.server.ts | 46 +++++++++++++++++++ src/services/logger.ts | 8 +++- vite.config.ts | 25 ++++++---- 15 files changed, 109 insertions(+), 31 deletions(-) rename src/services/{emailService.ts => emailService.server.ts} (99%) create mode 100644 src/services/logger.client.ts create mode 100644 src/services/logger.server.ts diff --git a/server.ts b/server.ts index 359f8dee..858f07b0 100644 --- a/server.ts +++ b/server.ts @@ -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'; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 02916880..8d0329b7 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -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(); diff --git a/src/routes/ai.ts b/src/routes/ai.ts index 57321a03..86b48b9e 100644 --- a/src/routes/ai.ts +++ b/src/routes/ai.ts @@ -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(); diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 81c4ead7..e666f1fd 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -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.' }); diff --git a/src/routes/passport.ts b/src/routes/passport.ts index f3559767..ac500e14 100644 --- a/src/routes/passport.ts +++ b/src/routes/passport.ts @@ -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.' }); } diff --git a/src/routes/public.ts b/src/routes/public.ts index 7a86b479..0931196e 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -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(); diff --git a/src/routes/system.ts b/src/routes/system.ts index cb5d912c..5c58b0e6 100644 --- a/src/routes/system.ts +++ b/src/routes/system.ts @@ -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(); diff --git a/src/routes/user.integration.test.ts b/src/routes/user.integration.test.ts index 65eb7a21..dc558f7a 100644 --- a/src/routes/user.integration.test.ts +++ b/src/routes/user.integration.test.ts @@ -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'; diff --git a/src/routes/user.ts b/src/routes/user.ts index 59eb4b3a..757011f7 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -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; } diff --git a/src/services/aiService.server.ts b/src/services/aiService.server.ts index 4f46b1dd..6c8bfbd0 100644 --- a/src/services/aiService.server.ts +++ b/src/services/aiService.server.ts @@ -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; diff --git a/src/services/emailService.ts b/src/services/emailService.server.ts similarity index 99% rename from src/services/emailService.ts rename to src/services/emailService.server.ts index fc1d0aee..611cbe6d 100644 --- a/src/services/emailService.ts +++ b/src/services/emailService.server.ts @@ -1,5 +1,5 @@ import nodemailer from 'nodemailer'; -import { logger } from './logger'; +import { logger } from './logger.server'; let transporter: nodemailer.Transporter; diff --git a/src/services/logger.client.ts b/src/services/logger.client.ts new file mode 100644 index 00000000..61611600 --- /dev/null +++ b/src/services/logger.client.ts @@ -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 = (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: (message: string, ...args: T) => log('INFO', message, ...args), + warn: (message: string, ...args: T) => log('WARN', message, ...args), + error: (message: string, ...args: T) => log('ERROR', message, ...args), + debug: (message: string, ...args: T) => log('DEBUG', message, ...args), +}; \ No newline at end of file diff --git a/src/services/logger.server.ts b/src/services/logger.server.ts new file mode 100644 index 00000000..56b863fd --- /dev/null +++ b/src/services/logger.server.ts @@ -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 `` + * 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 = (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: (message: string, ...args: T) => log('INFO', message, ...args), + warn: (message: string, ...args: T) => log('WARN', message, ...args), + error: (message: string, ...args: T) => log('ERROR', message, ...args), + debug: (message: string, ...args: T) => log('DEBUG', message, ...args), +}; \ No newline at end of file diff --git a/src/services/logger.ts b/src/services/logger.ts index 0d1c5529..716e3e5a 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -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 = (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': diff --git a/vite.config.ts b/vite.config.ts index 0ad9d6f5..a54d9ffc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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 ], }, },