some more re-org + fixes
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 1m1s

This commit is contained in:
2025-11-24 14:46:17 -08:00
parent ff2a82f06d
commit a4d5e95937
15 changed files with 109 additions and 31 deletions

View File

@@ -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';

View File

@@ -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();

View File

@@ -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();

View File

@@ -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.' });

View File

@@ -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.' });
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
import nodemailer from 'nodemailer';
import { logger } from './logger';
import { logger } from './logger.server';
let transporter: nodemailer.Transporter;

View 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),
};

View 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),
};

View File

@@ -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':

View File

@@ -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
],
},
},