curaflow/backend/server.js

236 lines
8.6 KiB
JavaScript

const express = require('express');
const path = require('path');
const botLogic = require('./botLogic');
const configManager = require('./configManager');
const { getTemporalClient } = require('./temporalClient');
const crypto = require('crypto');
const app = express();
const port = 3000;
app.use(express.json());
const staticRoot = path.join(__dirname, '../');
/** UTF-8 Content-Type for text assets (fixes emoji/₹ mojibake when proxy omits charset). */
function setUtf8ContentType(res, filePath) {
if (filePath.endsWith('.html')) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
} else if (filePath.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
} else if (filePath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
} else if (filePath.endsWith('.json')) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
}
}
function setNoCacheHeaders(res) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
function sendHtmlPage(res, filename) {
setNoCacheHeaders(res);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.sendFile(path.join(staticRoot, filename));
}
// Serve static files
app.use(express.static(staticRoot, {
setHeaders: (res, filePath) => {
setUtf8ContentType(res, filePath);
if (filePath.endsWith('.html')) {
setNoCacheHeaders(res);
}
}
}));
// Primary Routes
app.get('/', (req, res) => sendHtmlPage(res, 'login.html'));
app.get('/admin', (req, res) => sendHtmlPage(res, 'admin.html'));
app.get('/dashboard', (req, res) => sendHtmlPage(res, 'dashboard.html'));
// Multi-tenant Config API
app.get('/api/config/:tenantId', (req, res) => {
const config = configManager.getTenantConfig(req.params.tenantId);
res.json(config);
});
// Official Twilio Webhook parsing middleware
app.use(express.urlencoded({ extended: true }));
// WhatsApp Webhook (Twilio)
app.post('/whatsapp/webhook', async (req, res) => {
// Twilio sends urlencoded fields: From, Body
// Or our mock sends json: from, body
const from = req.body.From || req.body.from;
const body = req.body.Body || req.body.body;
console.log(`[WhatsApp Webhook] Received message from ${from}: ${body}`);
if (!from || !body) {
return res.status(400).send("Missing from or body");
}
try {
const temporalClient = await getTemporalClient();
// Use the phone number as the unique Workflow ID
const workflowId = `booking-${from.replace(/[^a-zA-Z0-9]/g, '')}`;
let handle;
try {
handle = temporalClient.workflow.getHandle(workflowId);
const desc = await handle.describe();
if (desc.status.name === 'RUNNING') {
// Ongoing conversation! Send a signal with the new message
console.log(`[WhatsApp Webhook] Signaling existing workflow ${workflowId}`);
await handle.signal('receive_message', body);
// Twilio expects a valid TwiML response. Empty response means no immediate automated reply.
// Our Temporal worker will asynchronously send a reply back via Twilio API.
res.type('text/xml').send('<Response></Response>');
return;
}
} catch (err) {
// Workflow does not exist. We will start a new one below.
}
console.log(`[WhatsApp Webhook] Starting new AppointmentBookingWorkflow for ${from}`);
await temporalClient.workflow.start('AppointmentBookingWorkflow', {
args: [body, from],
taskQueue: 'curaflow-tasks',
workflowId: workflowId,
});
res.type('text/xml').send('<Response></Response>');
} catch (err) {
console.error('[WhatsApp Webhook] Error processing message:', err);
res.status(500).send('Internal Server Error');
}
});
// AI Scribe Endpoints
app.post('/api/ai/scribe/start', async (req, res) => {
try {
const { dictation } = req.body;
if (!dictation) return res.status(400).json({ error: 'Dictation is required' });
const temporalClient = await getTemporalClient();
const jobId = `scribe-${crypto.randomUUID()}`;
console.log(`[AI Scribe] Starting job ${jobId} for dictation of length ${dictation.length}`);
// Start the workflow asynchronously
await temporalClient.workflow.start('ClinicalIntakeWorkflow', {
args: [dictation],
taskQueue: 'curaflow-tasks',
workflowId: jobId,
});
res.json({ success: true, jobId });
} catch (err) {
console.error('[AI Scribe] Start Error:', err);
res.status(500).json({ error: 'Failed to start AI Scribe job' });
}
});
app.get('/api/ai/scribe/status/:jobId', async (req, res) => {
try {
const { jobId } = req.params;
const temporalClient = await getTemporalClient();
const handle = temporalClient.workflow.getHandle(jobId);
const description = await handle.describe();
// Check if completed
if (description.status.name === 'COMPLETED') {
const result = await handle.result();
return res.json({ status: 'COMPLETED', result });
} else if (description.status.name === 'FAILED') {
return res.json({ status: 'FAILED' });
}
return res.json({ status: description.status.name });
} catch (err) {
// If not found or error
console.error(`[AI Scribe] Status Error for ${req.params.jobId}:`, err);
res.status(500).json({ error: 'Failed to get job status' });
}
});
// Lab Watcher Endpoints
app.post('/api/ai/lab/watch/start', async (req, res) => {
try {
const { patientId, baselineData } = req.body;
if (!patientId) return res.status(400).json({ error: 'Patient ID is required' });
const temporalClient = await getTemporalClient();
const workflowId = `lab-watcher-${patientId}`;
// Ensure we don't start it twice if it's already running
try {
const handle = temporalClient.workflow.getHandle(workflowId);
const desc = await handle.describe();
if (desc.status.name === 'RUNNING') {
return res.json({ success: true, message: 'Watcher already running' });
}
} catch (e) {
// Not found, so we proceed to start it
}
await temporalClient.workflow.start('LabResultWatcherWorkflow', {
args: [baselineData || { baseline: "Patient is healthy", medications: [] }],
taskQueue: 'curaflow-tasks',
workflowId: workflowId,
});
res.json({ success: true });
} catch (err) {
console.error('[AI Lab Watcher] Start Error:', err);
res.status(500).json({ error: 'Failed to start AI Lab Watcher' });
}
});
app.post('/api/ai/lab/result/:patientId', async (req, res) => {
try {
const { patientId } = req.params;
const { result } = req.body;
if (!result) return res.status(400).json({ error: 'Lab result string is required' });
const temporalClient = await getTemporalClient();
const workflowId = `lab-watcher-${patientId}`;
const handle = temporalClient.workflow.getHandle(workflowId);
await handle.signal('add_lab_result', result);
res.json({ success: true });
} catch (err) {
console.error(`[AI Lab Watcher] Signal Error for ${req.params.patientId}:`, err);
res.status(500).json({ error: 'Failed to send lab result to AI Watcher' });
}
});
app.get('/api/ai/lab/alerts/:patientId', async (req, res) => {
try {
const { patientId } = req.params;
const temporalClient = await getTemporalClient();
const workflowId = `lab-watcher-${patientId}`;
try {
const handle = temporalClient.workflow.getHandle(workflowId);
const alerts = await handle.query('get_alerts');
res.json({ success: true, alerts });
} catch (e) {
// Workflow might not be running
res.json({ success: true, alerts: [] });
}
} catch (err) {
console.error(`[AI Lab Watcher] Query Error for ${req.params.patientId}:`, err);
res.status(500).json({ error: 'Failed to fetch alerts' });
}
});
app.listen(port, '0.0.0.0', () => {
console.log(`Curio WhatsApp Mock Server running at http://0.0.0.0:${port}`);
});