import "dotenv/config"; import express from "express"; import cors from "cors"; import crypto from "crypto"; import path from "path"; import { fileURLToPath } from "url"; import { ensureTenant, listConversations, listRuns, getRunById } from "./src/db/repo.js"; import { addSseClient, removeSseClient, sseSend } from "./src/services/sse.js"; import { processMessage } from "./src/services/pipeline.js"; const app = express(); app.use(cors()); app.use(express.json({ limit: "1mb" })); // Serve /public as static (UI + webcomponents) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const publicDir = path.join(__dirname, "public"); app.use(express.static(publicDir)); /** * --- Tenant --- */ const TENANT_KEY = process.env.TENANT_KEY || "piaf"; let TENANT_ID = null; function nowIso() { return new Date().toISOString(); } /** * --- SSE endpoint --- */ app.get("/stream", (req, res) => { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.flushHeaders?.(); res.write(`event: hello\ndata: ${JSON.stringify({ ts: nowIso() })}\n\n`); addSseClient(res); req.on("close", () => { removeSseClient(res); res.end(); }); }); /** * --- Simulator --- * POST /sim/send { chat_id, from_phone, text } */ app.post("/sim/send", async (req, res) => { const { chat_id, from_phone, text } = req.body || {}; if (!chat_id || !from_phone || !text) { return res.status(400).json({ ok: false, error: "chat_id, from_phone, text are required" }); } try { const provider = "sim"; const message_id = crypto.randomUUID(); // idempotencia por mensaje sim const result = await processMessage({ tenantId: TENANT_ID, chat_id, from: from_phone, text, provider, message_id, }); res.json({ ok: true, run_id: result.run_id, reply: result.reply }); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "internal_error", detail: String(err?.message || err) }); } }); /** * --- UI data endpoints --- */ app.get("/conversations", async (req, res) => { const { q = "", status = "", state = "", limit = "50" } = req.query; try { const items = await listConversations({ tenant_id: TENANT_ID, q: String(q || ""), status: String(status || ""), state: String(state || ""), limit: parseInt(limit, 10) || 50, }); res.json({ items }); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "internal_error" }); } }); app.get("/runs", async (req, res) => { const { chat_id = null, limit = "50" } = req.query; try { const items = await listRuns({ tenant_id: TENANT_ID, wa_chat_id: chat_id ? String(chat_id) : null, limit: parseInt(limit, 10) || 50, }); res.json({ items }); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "internal_error" }); } }); app.get("/runs/:run_id", async (req, res) => { try { const run = await getRunById({ tenant_id: TENANT_ID, run_id: req.params.run_id }); if (!run) return res.status(404).json({ ok: false, error: "not_found" }); res.json(run); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "internal_error" }); } }); app.get("/", (req, res) => { res.sendFile(path.join(publicDir, "index.html")); }); /** * --- Boot --- */ const port = process.env.PORT || 3000; (async function boot() { TENANT_ID = await ensureTenant({ key: TENANT_KEY, name: TENANT_KEY.toUpperCase() }); app.listen(port, () => console.log(`UI: http://localhost:${port} (tenant=${TENANT_KEY})`)); })().catch((err) => { console.error("Boot failed:", err); process.exit(1); });