interacmanagernew / scripts /healthcheck.ts
MichaelEdou
Initial commit β€” ICC Interac Manager full-stack app
149698e
#!/usr/bin/env node
// scripts/healthcheck.ts
//
// ICC Interac Manager β€” Full Stack Health Check
// Run: npx tsx scripts/healthcheck.ts
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
const PASS = '\x1b[32mβœ… PASS\x1b[0m';
const FAIL = '\x1b[31m❌ FAIL\x1b[0m';
const WARN = '\x1b[33m⚠️ WARN\x1b[0m';
const INFO = '\x1b[36mℹ️ INFO\x1b[0m';
let passed = 0;
let failed = 0;
let warnings = 0;
function pass(msg: string) { console.log(` ${PASS} ${msg}`); passed++; }
function fail(msg: string, fix?: string) {
console.log(` ${FAIL} ${msg}`);
if (fix) console.log(` β†’ Fix: ${fix}`);
failed++;
}
function warn(msg: string) { console.log(` ${WARN} ${msg}`); warnings++; }
function info(msg: string) { console.log(` ${INFO} ${msg}`); }
function section(title: string) { console.log(`\n\x1b[1m━━━ ${title} ━━━\x1b[0m`); }
async function main() {
console.log('\n\x1b[1mπŸ” ICC Interac Manager β€” Health Check\x1b[0m');
console.log('═'.repeat(50));
// ═══════════════════════════════════════════
// 1. ENVIRONMENT VARIABLES
// ═══════════════════════════════════════════
section('1. Environment Variables');
const envPath = path.join(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
pass('.env file exists');
} else {
fail('.env file missing', 'cp .env.example .env && edit with your keys');
}
const requiredVars = [
{ key: 'VITE_GOOGLE_CLIENT_ID', fix: 'Get from https://console.cloud.google.com/apis/credentials' },
{ key: 'GOOGLE_CLIENT_SECRET', fix: 'Get from https://console.cloud.google.com/apis/credentials' },
];
const recommendedVars = [
{ key: 'GROQ_API_KEY', fix: 'Get free key at https://console.groq.com/keys' },
{ key: 'MISTRAL_API_KEY', fix: 'Get free key at https://console.mistral.ai/api-keys' },
{ key: 'JWT_SECRET', fix: 'Set a random string for production security' },
];
for (const v of requiredVars) {
if (process.env[v.key]) {
pass(`${v.key} is set`);
} else {
fail(`${v.key} is missing`, v.fix);
}
}
let hasAI = false;
for (const v of recommendedVars) {
if (process.env[v.key]) {
pass(`${v.key} is set`);
if (v.key.includes('API_KEY')) hasAI = true;
} else {
if (v.key.includes('API_KEY')) {
warn(`${v.key} not set β€” ${v.fix}`);
} else {
warn(`${v.key} not set (using default) β€” ${v.fix}`);
}
}
}
if (!hasAI) {
fail('No AI provider configured', 'Set at least GROQ_API_KEY or MISTRAL_API_KEY');
}
// ═══════════════════════════════════════════
// 2. DATABASE
// ═══════════════════════════════════════════
section('2. Database (SQLite)');
try {
const Database = require('better-sqlite3');
// Project uses sqlite.db relative to server CWD
const possiblePaths = [
path.join(process.cwd(), 'packages', 'server', 'sqlite.db'),
path.join(process.cwd(), 'sqlite.db'),
];
let dbPath = '';
for (const p of possiblePaths) {
if (fs.existsSync(p)) { dbPath = p; break; }
}
if (!dbPath) {
fail('SQLite database not found', 'Run the server once to auto-create tables');
} else {
pass(`SQLite database exists: ${dbPath}`);
const db = new Database(dbPath, { readonly: true });
pass('SQLite database opened');
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
).all().map((t: any) => t.name);
const expectedTables = ['users', 'transactions', 'branch_config', 'scan_logs'];
for (const table of expectedTables) {
if (tables.includes(table)) {
const count = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get() as any;
pass(`Table '${table}' exists (${count.c} rows)`);
} else {
fail(`Table '${table}' missing`, 'Run the server once to auto-create tables');
}
}
const indexes = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index'"
).all().map((i: any) => i.name);
info(`${indexes.length} indexes found`);
db.close();
}
} catch (error: any) {
fail(`Database error: ${error.message}`);
}
// ═══════════════════════════════════════════
// 3. GOOGLE OAUTH
// ═══════════════════════════════════════════
section('3. Google OAuth Configuration');
const clientId = process.env.VITE_GOOGLE_CLIENT_ID || process.env.GOOGLE_CLIENT_ID;
if (clientId) {
if (clientId.endsWith('.apps.googleusercontent.com')) {
pass('GOOGLE_CLIENT_ID format looks correct');
} else {
warn('GOOGLE_CLIENT_ID format unusual β€” should end with .apps.googleusercontent.com');
}
}
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3001/api/auth/google/callback';
info(`Redirect URI: ${redirectUri}`);
// ═══════════════════════════════════════════
// 4. AI PROVIDERS
// ═══════════════════════════════════════════
section('4. AI Provider Connectivity');
if (process.env.GROQ_API_KEY) {
try {
const res = await fetch('https://api.groq.com/openai/v1/models', {
headers: { Authorization: `Bearer ${process.env.GROQ_API_KEY}` },
});
if (res.ok) {
const data = await res.json();
pass(`Groq API connected (${data.data?.length || '?'} models available)`);
} else if (res.status === 401) {
fail('Groq API key is invalid (401)', 'Check key at https://console.groq.com/keys');
} else {
warn(`Groq API returned ${res.status}`);
}
} catch (e: any) {
fail(`Groq API unreachable: ${e.message}`);
}
} else {
warn('Groq not configured (skipping)');
}
if (process.env.MISTRAL_API_KEY) {
try {
const res = await fetch('https://api.mistral.ai/v1/models', {
headers: { Authorization: `Bearer ${process.env.MISTRAL_API_KEY}` },
});
if (res.ok) {
pass('Mistral API connected');
} else if (res.status === 401) {
fail('Mistral API key is invalid (401)', 'Check key at https://console.mistral.ai/api-keys');
} else {
warn(`Mistral API returned ${res.status}`);
}
} catch (e: any) {
fail(`Mistral API unreachable: ${e.message}`);
}
} else {
warn('Mistral not configured (skipping)');
}
// ═══════════════════════════════════════════
// 5. API SERVER
// ═══════════════════════════════════════════
section('5. API Server');
const port = process.env.PORT || '3001';
const baseUrl = `http://localhost:${port}`;
try {
const res = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
if (res.ok) {
pass(`Server running on port ${port}`);
const data = await res.json();
pass(`Health endpoint OK: ${JSON.stringify(data)}`);
} else {
fail(`Health endpoint returned ${res.status}`);
}
} catch (e: any) {
if (e.name === 'AbortError' || e.code === 'ECONNREFUSED') {
fail(`Server not running on port ${port}`, `Start with: cd packages/server && npx tsx src/index.ts`);
info('Skipping remaining server tests');
printSummary();
return;
}
fail(`Server error: ${e.message}`);
}
// Test unauthenticated access
try {
const res = await fetch(`${baseUrl}/api/transactions`);
if (res.status === 401) {
pass('Auth middleware blocks unauthenticated requests (401)');
} else {
warn(`/api/transactions returned ${res.status} without auth (expected 401)`);
}
} catch (e: any) {
fail(`Auth test error: ${e.message}`);
}
// Test OAuth redirect
try {
const res = await fetch(`${baseUrl}/api/auth/google`, { redirect: 'manual' });
if (res.status === 302 || res.status === 301) {
const location = res.headers.get('location') || '';
if (location.includes('accounts.google.com')) {
pass('OAuth redirect points to Google');
} else {
warn(`OAuth redirect goes to: ${location.substring(0, 80)}...`);
}
} else {
fail(`OAuth redirect returned ${res.status} (expected 302)`);
}
} catch (e: any) {
fail(`OAuth test error: ${e.message}`);
}
// ═══════════════════════════════════════════
// 6. WEBSOCKET (Socket.io)
// ═══════════════════════════════════════════
section('6. WebSocket (Socket.io)');
try {
// Socket.io uses HTTP polling first, then upgrades β€” test the polling endpoint
const sioRes = await fetch(`${baseUrl}/ws/socket.io/?EIO=4&transport=polling`, { signal: AbortSignal.timeout(3000) });
if (sioRes.ok) {
pass('Socket.io endpoint responds on /ws');
} else {
warn(`Socket.io endpoint returned ${sioRes.status}`);
}
} catch (e: any) {
warn(`Socket.io test: ${e.message} (may need server running with Socket.io)`);
}
// ═══════════════════════════════════════════
// 7. FILE STRUCTURE
// ═══════════════════════════════════════════
section('7. Project File Structure');
const criticalFiles = [
'packages/server/src/index.ts',
'packages/server/src/services/gmailService.ts',
'packages/server/src/services/aiService.ts',
'packages/server/src/services/scanService.ts',
'packages/server/src/services/chatService.ts',
'packages/server/src/routes/auth.ts',
'packages/server/src/routes/emails.ts',
'packages/server/src/routes/chat.ts',
'packages/server/src/routes/transactions.ts',
'packages/server/src/middleware/auth.ts',
'packages/server/src/db/index.ts',
'packages/server/src/db/schema.ts',
'packages/server/src/websocket/scanEvents.ts',
'packages/web/src/components/chat/ChatAssistant.tsx',
'packages/web/src/hooks/useEmailScan.ts',
'packages/web/src/hooks/useChatAssistant.ts',
'packages/web/src/services/api.ts',
'packages/web/src/services/websocket.ts',
];
for (const file of criticalFiles) {
const fullPath = path.join(process.cwd(), file);
if (fs.existsSync(fullPath)) {
pass(file);
} else {
fail(`${file} missing`);
}
}
// ═══════════════════════════════════════════
// 8. VITE CONFIG
// ═══════════════════════════════════════════
section('8. Vite Proxy Configuration');
const viteConfigPath = path.join(process.cwd(), 'packages', 'web', 'vite.config.ts');
if (fs.existsSync(viteConfigPath)) {
const content = fs.readFileSync(viteConfigPath, 'utf-8');
pass('Vite config found');
if (content.includes('/api')) {
pass('API proxy configured (/api β†’ backend)');
} else {
fail('Missing /api proxy in Vite config');
}
if (content.includes('/ws')) {
pass('WebSocket proxy configured');
} else {
fail('Missing /ws proxy in Vite config');
}
} else {
warn('No vite.config.ts found');
}
printSummary();
}
function printSummary() {
console.log('\n' + '═'.repeat(50));
console.log('\x1b[1mπŸ“‹ SUMMARY\x1b[0m');
console.log(` ${PASS.replace(' PASS', '')} Passed: ${passed}`);
console.log(` ${FAIL.replace(' FAIL', '')} Failed: ${failed}`);
console.log(` ${WARN.replace(' WARN', '')} Warnings: ${warnings}`);
console.log('═'.repeat(50));
if (failed === 0) {
console.log('\n\x1b[32mπŸŽ‰ All critical checks passed! Your app should work.\x1b[0m\n');
} else {
console.log(`\n\x1b[31mπŸ”§ Fix the ${failed} failing check(s) above before testing further.\x1b[0m\n`);
}
process.exit(failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error('Health check crashed:', err);
process.exit(1);
});