| import express from 'express'; |
| import type { Request, Response } from 'express'; |
| import { google } from 'googleapis'; |
| import fs from 'fs'; |
| import 'dotenv/config'; |
| import path from 'path'; |
| import { spawnSync, spawn } from 'child_process'; |
| import multer from 'multer'; |
| import sqlite3 from 'sqlite3'; |
|
|
| const app = express(); |
| app.use(express.json()); |
| app.use((_req: Request, res: Response, next: () => void) => { |
| console.log(`[${new Date().toISOString()}] ${_req.method} ${_req.url}`); |
| res.header('Access-Control-Allow-Origin', '*'); |
| res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE'); |
| res.header('Access-Control-Allow-Headers', 'Content-Type'); |
| if (_req.method === 'OPTIONS') { res.status(200).end(); return; } |
| next(); |
| }); |
|
|
| console.log('Backend Started !!!'); |
| |
| const PORT = process.env.SPACE_ID ? '7860' : (process.env.PORT || 8000); |
| const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(process.cwd(), '..'); |
| const PYTHON_EXE = process.env.PYTHON_EXE || path.join(WORKSPACE_ROOT, 'qwen_env/bin/python3'); |
| const PYTHON_DIR = path.resolve(WORKSPACE_ROOT, 'AgenticControl'); |
| const CHECK_USER_CLI = path.join(PYTHON_DIR, 'check_user_cli.py'); |
| const SAVE_USER_CLI = path.join(PYTHON_DIR, 'save_user_cli.py'); |
| const UPDATE_EMAIL_CLI = path.join(PYTHON_DIR, 'update_email_cli.py'); |
| const UPDATE_METADATA_CLI = path.join(PYTHON_DIR, 'update_client_email.py'); |
| const EMAIL_GENERATOR_CLI = path.join(PYTHON_DIR, 'EmailGenerator.py'); |
| const REVIEW_AGENT_CLI = path.join(PYTHON_DIR, 'ReviewAndHeaderAgent.py'); |
| const MOVE_EMAIL_CLI = path.join(PYTHON_DIR, 'Send_email_db.py'); |
| const APPROVE_FOLLOWUP_CLI = path.join(PYTHON_DIR, 'approve_followup_db.py'); |
| const SEND_EMAIL_CLI = path.join(PYTHON_DIR, 'send_and_move_email_cli.py'); |
| const SEND_FOLLOWUP_CLI = path.join(PYTHON_DIR, 'send_and_move_followup_cli.py'); |
| const GENERATE_FOLLOWUP_CLI = path.join(PYTHON_DIR, 'Handling_FollowUp.py'); |
| const PROCESS_EXCEL_CLI = path.join(PYTHON_DIR, 'process_excel_cli.py'); |
| const READ_INBOX_CLI = path.join(PYTHON_DIR, 'read_inbox.py'); |
| const DRAFT_REPLY_CLI = path.join(PYTHON_DIR, 'draft_reply.py'); |
| const EXCEL_PROCESSOR_CLI = path.join(PYTHON_DIR, 'Excel_Processor.py'); |
|
|
| |
| const SWITCH_FILE = path.resolve('send_switch.json'); |
|
|
| function getSendSwitch(): boolean { |
| try { |
| if (!fs.existsSync(SWITCH_FILE)) return false; |
| const raw = JSON.parse(fs.readFileSync(SWITCH_FILE, 'utf-8')); |
| return raw.enabled === true; |
| } catch { return false; } |
| } |
|
|
| function setSendSwitch(enabled: boolean) { |
| fs.writeFileSync(SWITCH_FILE, JSON.stringify({ enabled, updated_at: new Date().toISOString() }, null, 2)); |
| console.log(`[SendSwitch] Email sending ${enabled ? 'ENABLED' : 'DISABLED'}`); |
| } |
|
|
|
|
| |
| const DB_TRACKING = path.join(WORKSPACE_ROOT, 'Database/EmailsUnderReview/emailsUnderReview.db'); |
| const DB_FOLLOWUP_REVIEW = path.join(WORKSPACE_ROOT, 'Database/EmailsUnderReview/followups_under_review.db'); |
| const RE_TRACKING = path.join(WORKSPACE_ROOT, 'Database/EmailsSent/email_to_be_sent.db'); |
| const DB_FOLLOWUP_SENT = path.join(WORKSPACE_ROOT, 'Database/EmailsSent/followups_sent.db'); |
| const DB_FOLLOWUPS_JOURNEY = path.join(WORKSPACE_ROOT, 'Database/FollowUps/sent_emails.db'); |
| const DB_INBOX = path.join(WORKSPACE_ROOT, 'Database/Inbox/inbox.db'); |
|
|
| |
| const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''; |
| const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''; |
| |
| const IS_HF = !!process.env.SPACE_ID; |
| const HF_URL = IS_HF ? `https://${process.env.SPACE_ID.replace('/', '-')}.hf.space` : null; |
|
|
| |
| |
| const DEFAULT_REDIRECT = HF_URL ? `${HF_URL}/api/auth/google/callback` : 'http://localhost:8000/api/auth/google/callback'; |
| const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || DEFAULT_REDIRECT; |
|
|
| console.log(`π‘ Redirect URI configured as: ${GOOGLE_REDIRECT_URI}`); |
|
|
| |
| const DB_FILE = path.resolve('database.json'); |
|
|
| function saveToDatabase(tokens: any, userInfo: { name: string; email: string }) { |
| const data = { |
| user_id: userInfo.email, |
| name: userInfo.name, |
| email: userInfo.email, |
| access_token: tokens.access_token, |
| refresh_token: tokens.refresh_token, |
| updated_at: new Date().toISOString(), |
| }; |
| fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); |
| console.log(`πΎ Session saved: ${userInfo.name} <${userInfo.email}>`); |
| } |
|
|
| function readSession() { |
| try { |
| if (!fs.existsSync(DB_FILE)) return null; |
| const d = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8')); |
| return d.email ? d : null; |
| } catch { return null; } |
| } |
|
|
| |
| function getPythonSpawnOptions(extraEnv: Record<string, string> = {}) { |
| return { |
| cwd: WORKSPACE_ROOT, |
| env: { |
| ...process.env, |
| WORKSPACE_ROOT: WORKSPACE_ROOT, |
| PYTHONPATH: [ |
| path.join(WORKSPACE_ROOT, 'src_2', 'ColdEmail'), |
| path.join(WORKSPACE_ROOT, 'AgenticControl'), |
| path.join(WORKSPACE_ROOT, 'USA_ImportYeti'), |
| process.env.PYTHONPATH || '' |
| ].filter(Boolean).join(':'), |
| ...extraEnv |
| } |
| }; |
| } |
| function checkUserExists(email: string): boolean { |
| const result = spawnSync(PYTHON_EXE, [CHECK_USER_CLI, email], { encoding: 'utf-8' }); |
| if (result.error) { console.error('Python check error:', result.error); return false; } |
| try { |
| const out = JSON.parse(result.stdout.trim()); |
| console.log(`π check_user_exists(${email}) β`, out.exists); |
| return out.exists === true; |
| } catch { return false; } |
| } |
|
|
| |
| |
| function saveUserProfileViaPython(payload: object): Promise<{ ok: boolean; error?: string }> { |
| return new Promise((resolve) => { |
| console.log(`[ProfileSave] Spawning ${PYTHON_EXE} save_user_cli.py (async)...`); |
| const child = spawn(PYTHON_EXE, [SAVE_USER_CLI], getPythonSpawnOptions()); |
|
|
| let stdout = ''; |
| let stderr = ''; |
| let resolved = false; |
|
|
| child.stdin.write(JSON.stringify(payload)); |
| child.stdin.end(); |
|
|
| child.stdout.on('data', (d: Buffer) => { |
| stdout += d.toString(); |
| |
| try { |
| const parsed = JSON.parse(stdout.trim()); |
| if (!resolved) { |
| resolved = true; |
| console.log('[ProfileSave] Received JSON response from Python, resolving early.'); |
| resolve(parsed); |
| } |
| } catch (e) { |
| |
| } |
| }); |
|
|
| child.stderr.on('data', (d: Buffer) => { |
| const line = d.toString().trim(); |
| if (line) console.log('[ProfileSave|py]', line); |
| stderr += line; |
| }); |
|
|
| child.on('close', (code: number | null, signal: string | null) => { |
| console.log(`[ProfileSave] Python exited β code=${code} signal=${signal}`); |
| if (!resolved) { |
| resolved = true; |
| const raw = stdout.trim(); |
| if (!raw) { |
| resolve({ ok: false, error: stderr || 'Python produced no output' }); |
| return; |
| } |
| try { |
| resolve(JSON.parse(raw)); |
| } catch { |
| console.error('[ProfileSave] Could not parse Python stdout:', raw); |
| resolve({ ok: false, error: stderr || raw }); |
| } |
| } |
| }); |
|
|
| child.on('error', (err: Error) => { |
| console.error('[ProfileSave] spawn error:', err); |
| if (!resolved) { |
| resolved = true; |
| resolve({ ok: false, error: err.message }); |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| export async function runPythonCli(scriptPath: string, payload: any, tag = 'PyCLI'): Promise<any> { |
| return new Promise((resolve) => { |
| const child = spawn(PYTHON_EXE, [scriptPath], getPythonSpawnOptions()); |
| let stdout = ''; |
| let stderr = ''; |
| let resolved = false; |
|
|
| child.stdin.write(JSON.stringify(payload)); |
| child.stdin.end(); |
|
|
| child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); |
| child.stderr.on('data', (d: Buffer) => { |
| const line = d.toString().trim(); |
| if (line) console.log(`[${tag}|py]`, line); |
| stderr += line; |
| }); |
|
|
| child.on('close', (code) => { |
| if (resolved) return; |
| resolved = true; |
| const raw = stdout.trim(); |
| |
| const lines = raw.split('\n').reverse(); |
| for (const line of lines) { |
| try { resolve(JSON.parse(line)); return; } catch { } |
| } |
| resolve({ ok: false, error: stderr || raw || `Process exited with code ${code}` }); |
| }); |
|
|
| child.on('error', (err: Error) => { |
| if (!resolved) { resolved = true; resolve({ ok: false, error: err.message }); } |
| }); |
| }); |
| } |
|
|
| |
| export const runPythonJSON = runPythonCli; |
|
|
| |
| const upload = multer({ storage: multer.memoryStorage() }); |
|
|
| |
| app.post('/api/profile/save', |
| upload.single('cv_file'), |
| async (req: Request, res: Response) => { |
| console.log('[ProfileSave] POST /api/profile/save received'); |
|
|
| try { |
| const session = readSession(); |
| if (!session) { res.status(401).json({ error: 'Not authenticated' }); return; } |
|
|
| const { name, location, github_description, languages } = req.body; |
| const cvFile = (req as any).file as Express.Multer.File | undefined; |
|
|
| const payload = { |
| email: session.email, |
| name: name || session.name, |
| location: location || '', |
| github_description: github_description || '', |
| languages: JSON.parse(languages || '[]'), |
| cv_file_b64: cvFile ? cvFile.buffer.toString('base64') : '', |
| cv_filename: cvFile ? cvFile.originalname : '', |
| access_token: session.access_token || '', |
| refresh_token: session.refresh_token || '', |
| }; |
|
|
| console.log('[ProfileSave] Saving profile for:', session.email, '| cv:', cvFile?.originalname ?? 'none'); |
| const result = await saveUserProfileViaPython(payload); |
|
|
| if (result.ok) { |
| res.json({ ok: true }); |
| } else { |
| console.error('[ProfileSave] Python error:', result.error); |
| res.status(500).json({ error: result.error }); |
| } |
| } catch (err) { |
| console.error('[ProfileSave] Unexpected error:', err); |
| res.status(500).json({ error: 'Failed to save profile' }); |
| } |
| } |
| ); |
|
|
|
|
|
|
| |
| app.post('/api/upload-excel', |
| upload.single('excel_file'), |
| (req: Request, res: Response) => { |
| console.log('[ExcelUpload] POST /api/upload-excel received'); |
|
|
| try { |
| const file = req.file; |
| if (!file) { |
| res.status(400).json({ error: 'No file uploaded' }); |
| return; |
| } |
|
|
| |
| const tempFilePath = path.join('/tmp', `excel_upload_${Date.now()}_${file.originalname}`); |
| fs.writeFileSync(tempFilePath, file.buffer); |
| console.log(`[ExcelUpload] Saved file to ${tempFilePath}, spawning process_excel_cli.py...`); |
|
|
| const child = spawn(PYTHON_EXE, [PROCESS_EXCEL_CLI, tempFilePath], getPythonSpawnOptions()); |
|
|
| child.stdout.on('data', (d: Buffer) => { |
| console.log(`[ExcelUpload|py] ${d.toString().trim()}`); |
| }); |
| child.stderr.on('data', (d: Buffer) => { |
| console.log(`[ExcelUpload|err] ${d.toString().trim()}`); |
| }); |
| child.on('close', (code) => { |
| console.log(`[ExcelUpload] Python finished with code ${code}`); |
| }); |
|
|
| |
| res.json({ ok: true, message: "Excel uploaded and processing started in background." }); |
|
|
| } catch (err) { |
| console.error('[ExcelUpload] Error:', err); |
| res.status(500).json({ error: 'Failed to upload excel file' }); |
| } |
| } |
| ); |
|
|
|
|
| let pipelineStatus = { |
| active: false, |
| startedAt: null as string | null, |
| lastExitCode: null as number | null |
| }; |
|
|
| |
| app.post('/api/pipeline/run-excel', (req: Request, res: Response) => { |
| if (pipelineStatus.active) { |
| res.status(400).json({ error: 'Pipeline is already running' }); |
| return; |
| } |
|
|
| console.log('\n' + 'β'.repeat(60)); |
| console.log('π [PIPELINE] RECEIVED TRIGGER FROM FRONTEND'); |
| console.log(`π Script: ${EXCEL_PROCESSOR_CLI}`); |
| console.log(`π Backend CWD: ${process.cwd()}`); |
| console.log(`π Python: ${PYTHON_EXE}`); |
| console.log('β'.repeat(60)); |
|
|
| pipelineStatus.active = true; |
| pipelineStatus.startedAt = new Date().toISOString(); |
| pipelineStatus.lastExitCode = null; |
|
|
| try { |
| const child = spawn(PYTHON_EXE, [EXCEL_PROCESSOR_CLI], getPythonSpawnOptions()); |
|
|
| child.stdout.on('data', (d: Buffer) => { |
| console.log(`[ExcelProcessor|out] ${d.toString().trim()}`); |
| }); |
|
|
| child.stderr.on('data', (d: Buffer) => { |
| console.error(`[ExcelProcessor|err] ${d.toString().trim()}`); |
| }); |
|
|
| child.on('error', (err) => { |
| console.error('β [PIPELINE] Failed to start process:', err); |
| pipelineStatus.active = false; |
| }); |
|
|
| child.on('close', (code) => { |
| console.log(`\nβ
[PIPELINE] Finished with code ${code}`); |
| pipelineStatus.active = false; |
| pipelineStatus.lastExitCode = code; |
| }); |
|
|
| res.json({ ok: true, message: "Excel Processor pipeline started." }); |
| } catch (err) { |
| console.error('β [PIPELINE] Unexpected error:', err); |
| pipelineStatus.active = false; |
| res.status(500).json({ error: 'Failed to start pipeline' }); |
| } |
| }); |
|
|
| app.get('/api/pipeline/status', (req: Request, res: Response) => { |
| res.json(pipelineStatus); |
| }); |
|
|
| |
| const oauth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI); |
|
|
| const SCOPES = [ |
| 'openid', |
| 'https://www.googleapis.com/auth/userinfo.email', |
| 'https://www.googleapis.com/auth/userinfo.profile', |
| 'https://www.googleapis.com/auth/gmail.modify', |
| 'https://mail.google.com/', |
| 'https://www.googleapis.com/auth/calendar', |
| ]; |
|
|
| |
| app.get('/api/auth/google', (_req: Request, res: Response) => { |
| try { |
| const url = oauth2Client.generateAuthUrl({ access_type: 'offline', prompt: 'consent', scope: SCOPES }); |
| res.json({ url }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed to generate auth URL' }); |
| } |
| }); |
|
|
| |
| app.get('/api/auth/session', (_req: Request, res: Response) => { |
| const s = readSession(); |
| if (!s) { res.status(404).json({ error: 'No session' }); return; } |
| res.json({ name: s.name, email: s.email }); |
| }); |
|
|
| |
| app.get('/api/auth/logout', (_req: Request, res: Response) => { |
| fs.writeFileSync(DB_FILE, '{}'); |
| res.json({ ok: true }); |
| }); |
|
|
| |
| app.get('/api/auth/check-registered', (req: Request, res: Response) => { |
| const email = req.query.email as string | undefined; |
| if (!email) { res.status(400).json({ error: 'Missing email' }); return; } |
| const registered = checkUserExists(email); |
| res.json({ registered }); |
| }); |
|
|
| |
| app.get('/api/auth/google/callback', async (req: Request, res: Response) => { |
| const code = req.query.code as string | undefined; |
| if (!code) { res.status(400).send('Missing authorization code.'); return; } |
|
|
| try { |
| |
| const { tokens } = await oauth2Client.getToken(code); |
| oauth2Client.setCredentials(tokens); |
|
|
| |
| const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client }); |
| const { data: userInfo } = await oauth2.userinfo.get(); |
| const name = userInfo.name ?? 'Unknown'; |
| const email = userInfo.email ?? ''; |
| console.log(`π€ Google user: ${name} <${email}>`); |
|
|
| |
| saveToDatabase(tokens, { name, email }); |
|
|
| |
| const alreadyRegistered = checkUserExists(email); |
|
|
| const params = new URLSearchParams({ name, email }); |
|
|
| if (alreadyRegistered) { |
| |
| console.log(`β
Returning user detected β redirecting to /dashboard`); |
| params.set('page', 'dashboard'); |
| } else { |
| |
| console.log(`π New user detected β redirecting to /profile`); |
| params.set('page', 'profile'); |
| } |
|
|
| res.redirect(`${FRONTEND_URL}?google_connected=true&${params.toString()}`); |
|
|
| } catch (err) { |
| console.error('β OAuth callback error:', err); |
| res.redirect(`${FRONTEND_URL}?google_connected=false`); |
| } |
| }); |
|
|
| |
| app.post('/api/profile/save', |
| upload.single('cv_file'), |
| async (req: Request, res: Response) => { |
| console.log("Reached server.ts") |
|
|
| try { |
| const session = readSession(); |
| if (!session) { res.status(401).json({ error: 'Not authenticated' }); return; } |
|
|
| const { name, location, github_description, languages } = req.body; |
| const cvFile = (req as any).file as Express.Multer.File | undefined; |
|
|
| const payload = { |
| email: session.email, |
| name: name || session.name, |
| location: location || '', |
| github_description: github_description || '', |
| languages: JSON.parse(languages || '[]'), |
| cv_file_b64: cvFile ? cvFile.buffer.toString('base64') : '', |
| cv_filename: cvFile ? cvFile.originalname : '', |
| access_token: session.access_token || '', |
| refresh_token: session.refresh_token || '', |
| }; |
|
|
| console.log('πΎ Saving profile for:', session.email); |
| const result = await saveUserProfileViaPython(payload); |
|
|
| if (result.ok) { |
| res.json({ ok: true }); |
| } else { |
| console.error('Python save error:', result.error); |
| res.status(500).json({ error: result.error }); |
| } |
| } catch (err) { |
| console.error('β Profile save error:', err); |
| res.status(500).json({ error: 'Failed to save profile' }); |
| } |
| } |
| ); |
|
|
| |
| |
| app.get('/api/tracking', (req: Request, res: Response) => { |
| if (!fs.existsSync(DB_TRACKING)) { |
| res.status(404).json({ error: 'Database not found' }); |
| return; |
| } |
|
|
| |
| const db = new sqlite3.Database(DB_TRACKING, sqlite3.OPEN_READONLY, (err) => { |
| if (err) { |
| console.error('Database open error:', err); |
| res.status(500).json({ error: 'Failed to connect to database' }); |
| return; |
| } |
| }); |
|
|
| |
| db.all('SELECT * FROM tracking ORDER BY timestamp DESC', [], (err, rows) => { |
| if (err) { |
| console.error('Database query error:', err); |
| res.status(500).json({ error: 'Failed to query database' }); |
| } else { |
| res.json(rows); |
| } |
|
|
| |
| db.close(); |
| }); |
| }); |
|
|
| |
|
|
| app.get('/api/tracking_ready_to_send', (req: Request, res: Response) => { |
| if (!fs.existsSync(RE_TRACKING)) { |
| res.status(404).json({ error: 'Database not found' }); |
| return; |
| } |
|
|
| |
| const db = new sqlite3.Database(RE_TRACKING, sqlite3.OPEN_READONLY, (err) => { |
| if (err) { |
| console.error('Database open error:', err); |
| res.status(500).json({ error: 'Failed to connect to database' }); |
| return; |
| } |
| }); |
|
|
| |
| db.all('SELECT * FROM ready_emails ORDER BY timestamp DESC', [], (err, rows) => { |
| if (err) { |
| console.error('Database query error:', err); |
| res.status(500).json({ error: 'Failed to query database' }); |
| } else { |
| res.json(rows); |
| } |
|
|
| |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/tracking/:id', (req: Request, res: Response) => { |
| const id = req.params.id; |
| if (!fs.existsSync(DB_TRACKING)) { |
| res.status(404).json({ error: 'Database not found' }); |
| return; |
| } |
|
|
| const db = new sqlite3.Database(DB_TRACKING, sqlite3.OPEN_READONLY, (err) => { |
| if (err) { |
| console.error('Database open error:', err); |
| res.status(500).json({ error: 'Failed to connect to database' }); |
| return; |
| } |
| }); |
|
|
| db.get('SELECT * FROM tracking WHERE id = ?', [id], (err, row) => { |
| if (err) { |
| console.error('Database query error:', err); |
| res.status(500).json({ error: 'Failed to query database' }); |
| } else if (!row) { |
| res.status(404).json({ error: 'Record not found' }); |
| } else { |
| res.json(row); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.put('/api/tracking/:id/save', (req: Request, res: Response) => { |
| const id = req.params.id; |
| const { new_content, subject } = req.body; |
|
|
| if (!new_content) { |
| res.status(400).json({ error: 'Missing new_content' }); |
| return; |
| } |
|
|
| const db = new sqlite3.Database(DB_TRACKING, sqlite3.OPEN_READWRITE, (err) => { |
| if (err) { |
| console.error('Database open error:', err); |
| res.status(500).json({ error: 'Failed to connect to database' }); |
| return; |
| } |
| }); |
|
|
| db.get('SELECT body_json FROM tracking WHERE id = ?', [id], (err, row: any) => { |
| if (err || !row) { |
| db.close(); |
| res.status(404).json({ error: 'Record not found' }); |
| return; |
| } |
|
|
| try { |
| const payload = JSON.parse(row.body_json); |
| if (payload.body) payload.body.generated_content = new_content; |
| if (payload.body && subject) payload.body.subject = subject; |
|
|
| const updateQuery = subject |
| ? 'UPDATE tracking SET body_json = ?, generated_subject = ? WHERE id = ?' |
| : 'UPDATE tracking SET body_json = ? WHERE id = ?'; |
| const params = subject |
| ? [JSON.stringify(payload), subject, id] |
| : [JSON.stringify(payload), id]; |
|
|
| db.run(updateQuery, params, function (err) { |
| if (err) { |
| res.status(500).json({ error: 'Failed to update database' }); |
| } else { |
| res.json({ ok: true }); |
| } |
| db.close(); |
| }); |
| } catch (e) { |
| db.close(); |
| res.status(500).json({ error: 'JSON parsing error in DB' }); |
| } |
| }); |
| }); |
|
|
| |
| app.put('/api/tracking/:id/status', async (req: Request, res: Response) => { |
| const id = req.params.id; |
| const { status } = req.body; |
|
|
| if (status.toLowerCase() === 'approved') { |
| try { |
| const out = await runPythonCli(MOVE_EMAIL_CLI, { id }, 'MoveEmail'); |
| return res.json(out); |
| } catch (e) { |
| return res.status(500).json({ ok: false, error: String(e) }); |
| } |
| } |
|
|
| if (!status) return res.status(400).json({ error: 'Status is required' }); |
|
|
| const db = new sqlite3.Database(DB_TRACKING); |
| db.run('UPDATE tracking SET status = ? WHERE id = ?', [status, id], function (err) { |
| if (err) { |
| res.status(500).json({ error: 'Failed to update status' }); |
| } else { |
| res.json({ ok: true }); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| |
| app.get('/api/review/all', (_req: Request, res: Response) => { |
| const coldExists = fs.existsSync(DB_TRACKING); |
| const followupExists = fs.existsSync(DB_FOLLOWUP_REVIEW); |
|
|
| let coldRows: any[] = []; |
| let followupRows: any[] = []; |
| let pending = 0; |
|
|
| const finish = () => { |
| if (pending > 0) return; |
| const merged = [ |
| ...coldRows.map(r => ({ ...r, type: 'cold' })), |
| ...followupRows.map(r => ({ ...r, type: 'followup' })), |
| ].sort((a, b) => { |
| const ta = a.timestamp || a.followup_date || ''; |
| const tb = b.timestamp || b.followup_date || ''; |
| return tb.localeCompare(ta); |
| }); |
| res.json(merged); |
| }; |
|
|
| if (coldExists) { |
| pending++; |
| const db = new sqlite3.Database(DB_TRACKING, sqlite3.OPEN_READONLY); |
| db.all('SELECT * FROM tracking ORDER BY timestamp DESC', [], (err, rows) => { |
| if (!err) coldRows = rows as any[]; |
| db.close(); |
| pending--; |
| finish(); |
| }); |
| } |
|
|
| if (followupExists) { |
| pending++; |
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW, sqlite3.OPEN_READONLY); |
| db.all('SELECT * FROM followups_pending ORDER BY followup_date DESC', [], (err, rows) => { |
| if (!err) followupRows = rows as any[]; |
| db.close(); |
| pending--; |
| finish(); |
| }); |
| } |
|
|
| if (!coldExists && !followupExists) { |
| res.json([]); |
| } else if (pending === 0) { |
| finish(); |
| } |
| }); |
|
|
| |
| app.get('/api/settings/send-switch', (_req: Request, res: Response) => { |
| res.json({ enabled: getSendSwitch() }); |
| }); |
|
|
| |
| app.post('/api/settings/send-switch', (req: Request, res: Response) => { |
| const { enabled } = req.body; |
| if (typeof enabled !== 'boolean') { |
| res.status(400).json({ error: 'Body must be { "enabled": true|false }' }); |
| return; |
| } |
| setSendSwitch(enabled); |
| res.json({ ok: true, enabled }); |
| }); |
|
|
| |
| app.put('/api/review/cold/:id/approve', async (req: Request, res: Response) => { |
| const id = req.params.id; |
|
|
| |
| const db = new sqlite3.Database(DB_TRACKING, sqlite3.OPEN_READONLY); |
| const row: any = await new Promise((resolve) => { |
| db.get('SELECT company_email FROM tracking WHERE id = ?', [id], (err, r) => resolve(r)); |
| }); |
| db.close(); |
|
|
| if (row && row.company_email === 'not updated') { |
| return res.status(400).json({ ok: false, code: 'EMAIL_REQUIRED', message: 'Recipient email is missing. Please provide one.' }); |
| } |
|
|
| const cli = getSendSwitch() ? SEND_EMAIL_CLI : MOVE_EMAIL_CLI; |
| console.log(`[Approve Cold] id=${id} sendSwitch=${getSendSwitch()} cli=${path.basename(cli)}`); |
| try { |
| const out = await runPythonCli(cli, { id: parseInt(id as string) }, 'ApproveCold'); |
| return res.json(out); |
| } catch (e) { |
| return res.status(500).json({ ok: false, error: String(e) }); |
| } |
| }); |
|
|
|
|
| |
| app.put('/api/review/cold/:id/reject', (req: Request, res: Response) => { |
| const id = req.params.id; |
| const db = new sqlite3.Database(DB_TRACKING); |
| db.run('UPDATE tracking SET status = ? WHERE id = ?', ['rejected', id], function (err) { |
| if (err) res.status(500).json({ error: 'Failed to update status' }); |
| else res.json({ ok: true }); |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/review/followup/:id', (req: Request, res: Response) => { |
| const id = req.params.id; |
| if (!fs.existsSync(DB_FOLLOWUP_REVIEW)) { |
| res.status(404).json({ error: 'Follow-up database not found' }); |
| return; |
| } |
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW, sqlite3.OPEN_READONLY); |
| db.get('SELECT * FROM followups_pending WHERE id = ?', [id], (err, row) => { |
| if (err) res.status(500).json({ error: 'Query failed' }); |
| else if (!row) res.status(404).json({ error: 'Record not found' }); |
| else res.json(row); |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.put('/api/review/followup/:id/save', (req: Request, res: Response) => { |
| const id = req.params.id; |
| const { new_content, subject } = req.body; |
| if (!new_content) { res.status(400).json({ error: 'Missing new_content' }); return; } |
|
|
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW, sqlite3.OPEN_READWRITE); |
| db.get('SELECT body_json FROM followups_pending WHERE id = ?', [id], (err, row: any) => { |
| if (err || !row) { db.close(); res.status(404).json({ error: 'Record not found' }); return; } |
| try { |
| const payload = JSON.parse(row.body_json); |
| if (payload.body) payload.body.generated_content = new_content; |
| if (payload.body && subject) payload.body.subject = subject; |
|
|
| const updateQuery = subject |
| ? 'UPDATE followups_pending SET body_json = ?, generated_subject = ? WHERE id = ?' |
| : 'UPDATE followups_pending SET body_json = ? WHERE id = ?'; |
| const params = subject |
| ? [JSON.stringify(payload), subject, id] |
| : [JSON.stringify(payload), id]; |
|
|
| db.run(updateQuery, params, function (err2) { |
| if (err2) res.status(500).json({ error: 'Failed to save' }); |
| else res.json({ ok: true }); |
| db.close(); |
| }); |
| } catch { |
| db.close(); |
| res.status(500).json({ error: 'JSON parse error' }); |
| } |
| }); |
| }); |
|
|
| |
| app.put('/api/review/followup/:id/approve', async (req: Request, res: Response) => { |
| const id = req.params.id; |
|
|
| |
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW, sqlite3.OPEN_READONLY); |
| const row: any = await new Promise((resolve) => { |
| db.get('SELECT company_email FROM followups_pending WHERE id = ?', [id], (err, r) => resolve(r)); |
| }); |
| db.close(); |
|
|
| if (row && row.company_email === 'not updated') { |
| return res.status(400).json({ ok: false, code: 'EMAIL_REQUIRED', message: 'Recipient email is missing. Please provide one.' }); |
| } |
|
|
| const cli = getSendSwitch() ? SEND_FOLLOWUP_CLI : APPROVE_FOLLOWUP_CLI; |
| console.log(`[Approve Followup] id=${id} sendSwitch=${getSendSwitch()} cli=${path.basename(cli)}`); |
| try { |
| const out = await runPythonCli(cli, { id: parseInt(id as string) }, 'ApproveFollowup'); |
| return res.json(out); |
| } catch (e) { |
| return res.status(500).json({ ok: false, error: String(e) }); |
| } |
| }); |
|
|
|
|
| |
| app.put('/api/review/followup/:id/reject', (_req: Request, res: Response) => { |
| const id = _req.params.id; |
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW, sqlite3.OPEN_READWRITE); |
| db.run('UPDATE followups_pending SET status = ? WHERE id = ?', ['rejected', id], function (err) { |
| if (err) res.status(500).json({ error: 'Failed to update status' }); |
| else res.json({ ok: true }); |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.delete('/api/review/cold/:id', (req: Request, res: Response) => { |
| const id = req.params.id; |
| const db = new sqlite3.Database(DB_TRACKING); |
| db.run('DELETE FROM tracking WHERE id = ?', [id], function (err) { |
| if (err) res.status(500).json({ error: 'Failed to delete record' }); |
| else res.json({ ok: true }); |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.delete('/api/review/followup/:id', (req: Request, res: Response) => { |
| const id = req.params.id; |
| const db = new sqlite3.Database(DB_FOLLOWUP_REVIEW); |
| db.run('DELETE FROM followups_pending WHERE id = ?', [id], function (err) { |
| if (err) res.status(500).json({ error: 'Failed to delete record' }); |
| else res.json({ ok: true }); |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/followups/journey', (_req: Request, res: Response) => { |
| if (!fs.existsSync(DB_FOLLOWUPS_JOURNEY)) { |
| res.json([]); |
| return; |
| } |
| const db = new sqlite3.Database(DB_FOLLOWUPS_JOURNEY, sqlite3.OPEN_READONLY); |
| |
| db.all('SELECT * FROM sent_applications ORDER BY date_sent DESC', [], (err, rows) => { |
| if (err) { |
| console.error('β [JourneyAPI] DB Error:', err); |
| res.status(500).json({ error: 'Query failed' }); |
| } else { |
| res.json(rows); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/followups/sent-approved', (_req: Request, res: Response) => { |
| if (!fs.existsSync(DB_FOLLOWUP_SENT)) { |
| res.json([]); |
| return; |
| } |
| const db = new sqlite3.Database(DB_FOLLOWUP_SENT, sqlite3.OPEN_READONLY); |
| db.all('SELECT * FROM sent_followups ORDER BY approved_at DESC', [], (err, rows) => { |
| if (err) { |
| console.error('β [SentApprovedAPI] DB Error:', err); |
| res.status(500).json({ error: 'Query failed' }); |
| } else { |
| res.json(rows); |
| } |
| db.close(); |
| }); |
| }); |
|
|
|
|
| |
|
|
| app.get('/api/inbox', (req: Request, res: Response) => { |
| if (!fs.existsSync(DB_INBOX)) { |
| res.json([]); |
| return; |
| } |
| const db = new sqlite3.Database(DB_INBOX, sqlite3.OPEN_READONLY); |
| db.all('SELECT * FROM inbox_threads ORDER BY date_received DESC', [], (err, rows) => { |
| if (err) { |
| console.error('β [InboxAPI] DB Error:', err); |
| res.status(500).json({ error: 'Query failed' }); |
| } else { |
| res.json(rows); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| const PO_BASE_DIR = path.join(WORKSPACE_ROOT, 'AgenticControl/PO:Quotation'); |
| const PO_PIPELINE_CLI = path.join(PO_BASE_DIR, 'run_po_pipeline.py'); |
| const PO_PREDICTOR_CLI = path.join(PO_BASE_DIR, 'quotation_predictor.py'); |
| const PO_CWD = PO_BASE_DIR; |
| const PO_DB_PATH = path.join(PO_BASE_DIR, 'po_database.db'); |
| const PRED_DB_PATH = path.join(PO_BASE_DIR, 'quotation_predictions.db'); |
|
|
| const poUpload = multer({ |
| storage: multer.diskStorage({ |
| destination: (req, file, cb) => { |
| const dir = path.join(WORKSPACE_ROOT, 'backend', 'uploads'); |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); |
| cb(null, dir); |
| }, |
| filename: (req, file, cb) => { |
| cb(null, Date.now() + '-' + file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')); |
| } |
| }) |
| }); |
|
|
| app.post('/api/po/process', poUpload.single('file'), (req: Request, res: Response) => { |
| if (!req.file) { |
| res.status(400).json({ error: 'No file uploaded' }); |
| return; |
| } |
|
|
| |
| poPipelineState = { |
| status: 'running', |
| phase: 'initializing', |
| startedAt: new Date().toISOString(), |
| log: [`Upload successful: ${req.file.originalname}`, 'Starting PO analysis pipeline...'] |
| }; |
|
|
| try { |
| const child = spawn(PYTHON_EXE, [PO_PIPELINE_CLI], getPythonSpawnOptions({ PO_CWD: PO_CWD })); |
| let stdout = ''; |
| let stderr = ''; |
|
|
| child.stdin.write(JSON.stringify({ file_path: req.file.path })); |
| child.stdin.end(); |
|
|
| child.stdout.on('data', (d: Buffer) => { |
| const msg = d.toString(); |
| stdout += msg; |
| addPOLog(msg); |
| }); |
|
|
| child.stderr.on('data', (d: Buffer) => { |
| const line = d.toString().trim(); |
| if (line) { |
| console.log('[PO_CLI]', line); |
| addPOLog(`[stderr] ${line}`); |
| } |
| stderr += line; |
| }); |
|
|
| child.on('close', (code: number | null) => { |
| poPipelineState.status = code === 0 ? 'done' : 'error'; |
| if (code !== 0) addPOLog(`Pipeline exited with code ${code}`); |
|
|
| try { |
| |
| const lines = stdout.trim().split('\n'); |
| let parsed = null; |
| for (let i = lines.length - 1; i >= 0; i--) { |
| if (lines[i].trim().startsWith('{')) { |
| try { |
| parsed = JSON.parse(lines[i].trim()); |
| break; |
| } catch (e) { } |
| } |
| } |
|
|
| if (parsed) { |
| addPOLog('Analysis complete. Processing results.'); |
| res.json(parsed); |
| } else { |
| addPOLog('Error: Failed to parse results from analysis script.'); |
| res.status(500).json({ error: 'Failed to parse python output', stderr }); |
| } |
| } catch (e: any) { |
| res.status(500).json({ error: e.message }); |
| } |
| }); |
| } catch (err) { |
| poPipelineState.status = 'error'; |
| addPOLog(`Internal Server Error: ${String(err)}`); |
| res.status(500).json({ error: 'Internal server error' }); |
| } |
| }); |
|
|
| |
| app.get('/api/po/pipeline-status', (_req: Request, res: Response) => { |
| res.json(poPipelineState); |
| }); |
|
|
| |
| app.get('/api/po/tables', (req: Request, res: Response) => { |
| if (!fs.existsSync(PO_DB_PATH)) { |
| res.status(404).json({ error: 'PO Database not found' }); |
| return; |
| } |
| const db = new sqlite3.Database(PO_DB_PATH, sqlite3.OPEN_READONLY); |
| db.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", (err, rows) => { |
| if (err) { |
| res.status(500).json({ error: err.message }); |
| } else { |
| res.json({ tables: rows.map((r: any) => r.name) }); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/po/table/:name', (req: Request, res: Response) => { |
| const tableName = req.params.name; |
| if (!fs.existsSync(PO_DB_PATH)) { |
| res.status(404).json({ error: 'PO Database not found' }); |
| return; |
| } |
| const db = new sqlite3.Database(PO_DB_PATH, sqlite3.OPEN_READONLY); |
| |
| db.all(`SELECT * FROM "${tableName}"`, (err, rows) => { |
| if (err) { |
| res.status(500).json({ error: err.message }); |
| } else { |
| res.json({ success: true, rows }); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.get('/api/po/predictions/:name', (req: Request, res: Response) => { |
| const tableName = req.params.name; |
| const predTableName = `${tableName}_predictions`; |
| if (!fs.existsSync(PRED_DB_PATH)) { |
| res.status(404).json({ error: 'Prediction Database not found' }); |
| return; |
| } |
| const db = new sqlite3.Database(PRED_DB_PATH, sqlite3.OPEN_READONLY); |
| |
| const query = ` |
| SELECT * FROM "${predTableName}" |
| WHERE id IN ( |
| SELECT MAX(id) FROM "${predTableName}" |
| GROUP BY item_no, description |
| ) |
| ORDER BY id ASC |
| `; |
| db.all(query, (err, rows) => { |
| if (err) { |
| |
| res.json({ success: true, predictions: [] }); |
| } else { |
| res.json({ success: true, predictions: rows }); |
| } |
| db.close(); |
| }); |
| }); |
|
|
| |
| app.post('/api/po/predict', async (req: Request, res: Response) => { |
| const { table_name } = req.body; |
| if (!table_name) { |
| res.status(400).json({ error: 'Missing table_name' }); |
| return; |
| } |
|
|
| try { |
| console.log(`[PO_Predict] Running predictor for table: ${table_name}`); |
| |
| const child = spawn(PYTHON_EXE, [PO_PREDICTOR_CLI, '--table', table_name], getPythonSpawnOptions({ PO_CWD: PO_CWD })); |
|
|
| let stdout = ''; |
| let stderr = ''; |
|
|
| child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); |
| child.stderr.on('data', (d: Buffer) => { |
| const line = d.toString().trim(); |
| if (line) console.log('[Predictor_CLI]', line); |
| stderr += line; |
| }); |
|
|
| child.on('close', async (code) => { |
| console.log(`[PO_Predict] Predictor finished with code ${code}`); |
|
|
| |
| |
| const predTableName = `${table_name}_predictions`; |
|
|
| if (!fs.existsSync(PRED_DB_PATH)) { |
| res.status(500).json({ error: 'Prediction database not found after script execution' }); |
| return; |
| } |
|
|
| const db = new sqlite3.Database(PRED_DB_PATH, sqlite3.OPEN_READONLY); |
| const query = ` |
| SELECT * FROM "${predTableName}" |
| WHERE id IN ( |
| SELECT MAX(id) FROM "${predTableName}" |
| GROUP BY item_no, description |
| ) |
| ORDER BY id ASC |
| `; |
| db.all(query, (err, rows) => { |
| if (err) { |
| res.status(500).json({ error: `Failed to fetch predictions: ${err.message}`, stderr }); |
| } else { |
| res.json({ success: true, predictions: rows }); |
| } |
| db.close(); |
| }); |
| }); |
| } catch (err: any) { |
| res.status(500).json({ error: err.message }); |
| } |
| }); |
|
|
|
|
| app.post('/api/inbox/sync', async (req: Request, res: Response) => { |
| try { |
| const result = await runPythonCli(READ_INBOX_CLI, {}, 'ReadInbox'); |
| if (result.error) { |
| res.status(500).json({ error: result.error }); |
| } else { |
| res.json({ success: true, new_threads: result.new_threads || [], count: result.fetched_count || 0 }); |
| } |
| } catch (err) { |
| console.error('β [InboxSync] error:', err); |
| res.status(500).json({ error: 'Sync failed' }); |
| } |
| }); |
|
|
| app.post('/api/inbox/:id/draft', async (req: Request, res: Response) => { |
| const id = req.params.id; |
| try { |
| const result = await runPythonCli(DRAFT_REPLY_CLI, { inbox_id: id }, 'DraftReply'); |
| if (result.error) { |
| res.status(500).json({ error: result.error }); |
| } else { |
| res.json({ success: true }); |
| } |
| } catch (err) { |
| console.error('β [DraftReply] error:', err); |
| res.status(500).json({ error: 'Draft failed' }); |
| } |
| }); |
|
|
| |
| function seedClientCredentials() { |
| const session = readSession(); |
| if (session?.access_token) { |
| oauth2Client.setCredentials({ |
| access_token: session.access_token, |
| refresh_token: session.refresh_token, |
| }); |
| console.log(`π [TokenRefresh] Seeded OAuth2 client with saved tokens (user: ${session.email})`); |
| } else { |
| console.log(`β οΈ [TokenRefresh] No saved session β skipping credential seed`); |
| } |
| } |
|
|
| |
| |
| function listenForTokenEvents() { |
| oauth2Client.on('tokens', (freshTokens) => { |
| const session = readSession(); |
| if (!session) { console.warn('β οΈ [TokenRefresh] tokens event fired but no session on disk'); return; } |
|
|
| const merged = { |
| access_token: freshTokens.access_token ?? session.access_token, |
| refresh_token: freshTokens.refresh_token ?? session.refresh_token, |
| }; |
| saveToDatabase(merged, { name: session.name, email: session.email }); |
| console.log(`β
[TokenRefresh] tokens event β fresh access_token saved to database.json`); |
| }); |
| } |
|
|
| |
| async function proactiveTokenRefresh() { |
| const session = readSession(); |
| if (!session?.refresh_token) { |
| console.log(`β οΈ [TokenRefresh] No refresh_token available β proactive refresh skipped`); |
| return; |
| } |
|
|
| try { |
| |
| oauth2Client.setCredentials({ |
| access_token: session.access_token, |
| refresh_token: session.refresh_token, |
| }); |
|
|
| const { credentials } = await oauth2Client.refreshAccessToken(); |
|
|
| const merged = { |
| access_token: credentials.access_token ?? session.access_token, |
| refresh_token: credentials.refresh_token ?? session.refresh_token, |
| }; |
| saveToDatabase(merged, { name: session.name, email: session.email }); |
| console.log(`π [TokenRefresh] Proactive refresh done β database.json updated (${new Date().toISOString()})`); |
| } catch (err: any) { |
| |
| |
| const cause = err?.response?.data?.error ?? err?.message ?? String(err); |
| if (cause === 'invalid_grant') { |
| console.warn(`β οΈ [TokenRefresh] Refresh token expired or revoked (invalid_grant). Re-login to renew.`); |
| } else { |
| console.error(`β [TokenRefresh] Proactive refresh failed:`, err); |
| } |
| } |
| } |
|
|
| |
| |
| app.post('/api/followups/generate/:id', async (req: Request, res: Response) => { |
| const id = req.params.id; |
| try { |
| const out = await runPythonCli(GENERATE_FOLLOWUP_CLI, { id }, 'FollowupGen'); |
| res.json(out); |
| } catch (e) { |
| res.status(500).json({ ok: false, error: String(e) }); |
| } |
| }); |
|
|
|
|
| |
| const HOST = '0.0.0.0'; |
| app.listen(Number(PORT), HOST, () => { |
| console.log(`π Backend Auth Server running on http://${HOST}:${PORT}`); |
| if (IS_HF) console.log(`π Hugging Face Space URL detected: ${HF_URL}`); |
| |
| seedClientCredentials(); |
| listenForTokenEvents(); |
| proactiveTokenRefresh(); |
| setInterval(proactiveTokenRefresh, 2 * 60 * 1000); |
| console.log(`β° [TokenRefresh] Proactive token refresh scheduled every 45 minutes`); |
|
|
| |
| function runReviewAgent() { |
| console.log(`β±οΈ Running background task: ${REVIEW_AGENT_CLI}`); |
| const agentProcess = spawn(PYTHON_EXE, [REVIEW_AGENT_CLI], getPythonSpawnOptions()); |
|
|
| agentProcess.stdout.on('data', (data) => { |
| console.log(`[ReviewAgent] ${data.toString().trim()}`); |
| }); |
|
|
| agentProcess.stderr.on('data', (data) => { |
| console.error(`[ReviewAgent Error] ${data.toString().trim()}`); |
| }); |
|
|
| agentProcess.on('close', (code) => { |
| console.log(`[ReviewAgent] Exited with code ${code}`); |
| }); |
| } |
|
|
| function runOutreachAgent() { |
| console.log(`β±οΈ Starting background Outreach Agent: ${path.join(PYTHON_DIR, 'OutreachAgent.py')}`); |
| |
| const outreachAgent = spawn(PYTHON_EXE, [path.join(PYTHON_DIR, 'OutreachAgent.py')], getPythonSpawnOptions()); |
|
|
| outreachAgent.stdout.on('data', (data) => { |
| console.log(`[OutreachAgent] ${data.toString().trim()}`); |
| }); |
|
|
| outreachAgent.stderr.on('data', (data) => { |
| console.error(`[OutreachAgent Error] ${data.toString().trim()}`); |
| }); |
|
|
| outreachAgent.on('close', (code) => { |
| console.log(`[OutreachAgent] Process closed with code ${code}. Restarting in 60s...`); |
| setTimeout(runOutreachAgent, 60000); |
| }); |
| } |
|
|
| |
| runReviewAgent(); |
| runOutreachAgent(); |
|
|
| |
| setInterval(runReviewAgent, 1 * 60 * 1000); |
|
|
| |
| function runMarketIntelPipeline() { |
| if (pipelineState.status === 'running') return; |
| console.log('β° [SystemClock] Triggering scheduled Market Intelligence run...'); |
| |
| |
| fetch(`http://localhost:${PORT}/api/stats/run-pipeline`, { method: 'POST' }).catch(() => {}); |
| } |
|
|
| |
| setInterval(() => { |
| if (Date.now() >= nextMarketIntelRunAt) { |
| runMarketIntelPipeline(); |
| } |
| }, 60 * 1000); |
| }); |
|
|
| |
| app.post('/api/tracking/:id/ai-edit', async (req: Request, res: Response) => { |
| const id = req.params.id; |
| const { feedback } = req.body; |
|
|
| if (!feedback) { |
| res.status(400).json({ error: 'Missing feedback' }); |
| return; |
| } |
|
|
| const payload = { id: parseInt(id as string), feedback }; |
|
|
| try { |
| const out = await runPythonCli(UPDATE_EMAIL_CLI, payload, 'AIEdit'); |
| res.json(out); |
| } catch (e) { |
| res.status(500).json({ ok: false, error: String(e) }); |
| } |
| }); |
|
|
| |
| app.post('/api/tracking/:id/metadata', async (req: Request, res: Response) => { |
| const id = req.params.id; |
| const { field, value } = req.body; |
|
|
| if (!field || value === undefined) { |
| res.status(400).json({ error: 'Missing field or value' }); |
| return; |
| } |
|
|
| const payload = { id: parseInt(id as string), field, value }; |
|
|
| try { |
| const out = await runPythonCli(UPDATE_METADATA_CLI, payload, 'MetadataUpdate'); |
| res.json(out); |
| } catch (e) { |
| res.status(500).json({ ok: false, error: String(e) }); |
| } |
| }); |
|
|
|
|
| |
| |
| |
|
|
| const STATS_BASE = path.join(WORKSPACE_ROOT, 'Excel_Generator/Stats_data_collection'); |
| const RISK_JSON = path.join(STATS_BASE, 'risk_factors.json'); |
| const WEATHER_JSON = path.join(STATS_BASE, 'Weather_Forecast', 'weather_strategic_predictions.json'); |
| const UPDATE_ALL = '/Volumes/ssd2/CV Project No 2 copy/Excel_Generator/update_all_stats.py'; |
| const STATS_PY = path.join(STATS_BASE, 'Stats.py'); |
| const WEATHER_PY = path.join(STATS_BASE, 'Weather_LLM_Strategy.py'); |
|
|
| |
| let pipelineState: { status: string; phase: string; startedAt: string | null; log: string[] } = { |
| status: 'idle', phase: '', startedAt: null, log: [], |
| }; |
|
|
| let poPipelineState: { status: string; phase: string; startedAt: string | null; log: string[] } = { |
| status: 'idle', phase: '', startedAt: null, log: [], |
| }; |
|
|
| |
| const INTEL_CLOCK_FILE = path.join(WORKSPACE_ROOT, 'Database/next_intel_run.json'); |
| let nextMarketIntelRunAt = Date.now() + 60 * 60 * 1000; |
|
|
| function loadIntelClock() { |
| try { |
| if (fs.existsSync(INTEL_CLOCK_FILE)) { |
| const raw = JSON.parse(fs.readFileSync(INTEL_CLOCK_FILE, 'utf-8')); |
| if (raw.nextRunAt > Date.now()) { |
| nextMarketIntelRunAt = raw.nextRunAt; |
| console.log(`β° [SystemClock] Restored schedule: Next run at ${new Date(nextMarketIntelRunAt).toLocaleTimeString()}`); |
| } |
| } |
| } catch { } |
| } |
|
|
| function saveIntelClock(time: number) { |
| try { |
| fs.writeFileSync(INTEL_CLOCK_FILE, JSON.stringify({ nextRunAt: time })); |
| } catch { } |
| } |
|
|
| loadIntelClock(); |
|
|
| app.post('/api/market/chat', async (req: Request, res: Response) => { |
| const { question } = req.body; |
| if (!question) return res.status(400).json({ error: 'Missing question' }); |
| try { |
| const RAG_SCRIPT = path.join(WORKSPACE_ROOT, 'AgenticControl/MarketRAG.py'); |
| const out = await runPythonCli(RAG_SCRIPT, [question], 'MarketChat'); |
| res.json({ answer: out }); |
| } catch (e) { |
| res.status(500).json({ error: String(e) }); |
| } |
| }); |
|
|
| function addPOLog(msg: string) { |
| const clean = msg.trim(); |
| if (clean) { |
| console.log(`[PO_LOG] ${clean}`); |
| poPipelineState.log.push(clean); |
| if (poPipelineState.log.length > 200) poPipelineState.log.shift(); |
| } |
| } |
|
|
| |
| app.get('/api/stats/risk-factors', (_req: Request, res: Response) => { |
| if (!fs.existsSync(RISK_JSON)) { res.status(404).json({ error: 'risk_factors.json not found' }); return; } |
| try { res.json(JSON.parse(fs.readFileSync(RISK_JSON, 'utf-8'))); } |
| catch { res.status(500).json({ error: 'Failed to read risk_factors.json' }); } |
| }); |
|
|
| |
| app.get('/api/stats/weather-strategy', (_req: Request, res: Response) => { |
| if (!fs.existsSync(WEATHER_JSON)) { res.status(404).json({ error: 'weather_strategic_predictions.json not found' }); return; } |
| try { res.json(JSON.parse(fs.readFileSync(WEATHER_JSON, 'utf-8'))); } |
| catch { res.status(500).json({ error: 'Failed to read weather_strategic_predictions.json' }); } |
| }); |
|
|
| |
| app.get('/api/stats/pipeline-status', (_req: Request, res: Response) => { |
| res.json(pipelineState); |
| }); |
|
|
| |
| app.get('/api/stats/next-run', (_req: Request, res: Response) => { |
| res.json({ nextRunAt: nextMarketIntelRunAt }); |
| }); |
|
|
| |
| app.post('/api/stats/run-pipeline', (_req: Request, res: Response) => { |
| if (pipelineState.status === 'running') { |
| res.json({ ok: false, message: 'Pipeline already running', state: pipelineState }); |
| return; |
| } |
|
|
| pipelineState = { status: 'running', phase: 'scrapers', startedAt: new Date().toISOString(), log: [] }; |
| nextMarketIntelRunAt = Date.now() + 60 * 60 * 1000; |
| saveIntelClock(nextMarketIntelRunAt); |
| res.json({ ok: true, message: 'Pipeline started', state: pipelineState }); |
|
|
| const addLog = (msg: string) => { |
| console.log(`[Pipeline] ${msg}`); |
| pipelineState.log.push(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); |
| if (pipelineState.log.length > 200) pipelineState.log.shift(); |
| }; |
|
|
| addLog('Phase 1: Running update_all_stats.py (scrapers)β¦'); |
|
|
| |
| const scrapers = spawn(PYTHON_EXE, [UPDATE_ALL], getPythonSpawnOptions()); |
| scrapers.stdout.on('data', (d: Buffer) => addLog(d.toString().trim())); |
| scrapers.stderr.on('data', (d: Buffer) => addLog(`[stderr] ${d.toString().trim()}`)); |
| scrapers.on('close', (code: number | null) => { |
| if (code !== 0) { |
| addLog(`update_all_stats.py exited with code ${code} β proceeding anyway`); |
| } else { |
| addLog('Phase 1 complete. Starting Phase 2 (Stats + Weather in parallel)β¦'); |
| } |
|
|
| pipelineState.phase = 'analysis'; |
|
|
| |
| let done = 0; |
| const check = () => { if (++done === 2) { pipelineState.status = 'done'; addLog('β
All done.'); } }; |
|
|
| addLog('Phase 2: Generating final Market Intelligence reportsβ¦'); |
| const statsProc = spawn(PYTHON_EXE, [STATS_PY], getPythonSpawnOptions()); |
| statsProc.stdout.on('data', (d: Buffer) => addLog(`[Stats] ${d.toString().trim()}`)); |
| statsProc.stderr.on('data', (d: Buffer) => addLog(`[Stats|err] ${d.toString().trim()}`)); |
| statsProc.on('close', (c: number | null) => { addLog(`Stats.py done (code ${c})`); check(); }); |
|
|
| const weatherProc = spawn(PYTHON_EXE, [WEATHER_PY], getPythonSpawnOptions()); |
| weatherProc.stdout.on('data', (d: Buffer) => addLog(`[Weather] ${d.toString().trim()}`)); |
| weatherProc.stderr.on('data', (d: Buffer) => addLog(`[Weather|err] ${d.toString().trim()}`)); |
| weatherProc.on('close', (c: number | null) => { addLog(`Weather_LLM_Strategy.py done (code ${c})`); check(); }); |
| }); |
| scrapers.on('error', (err: Error) => { addLog(`Spawn error: ${err.message}`); pipelineState.status = 'error'; }); |
| }); |
|
|
| |
| |
| const FRONTEND_DIST = path.resolve(WORKSPACE_ROOT, 'frontend/dist'); |
| if (fs.existsSync(FRONTEND_DIST)) { |
| app.use(express.static(FRONTEND_DIST)); |
| app.get('/{*path}', (req: Request, res: Response) => { |
| if (req.path.startsWith('/api/')) { |
| res.status(404).json({ error: `API route not found: ${req.path}` }); |
| return; |
| } |
| res.sendFile(path.join(FRONTEND_DIST, 'index.html')); |
| }); |
| console.log(`π Serving frontend from: ${FRONTEND_DIST}`); |
| } else { |
| console.log(`β οΈ Frontend not built. Run: cd frontend && npm run build`); |
| } |