Spaces:
Running
Running
| // 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); | |
| }); | |