#!/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); });