feat: add in-browser dial pad with Twilio Voice SDK

- New DialPad component on Patient Connection page: clickable keypad,
  call/hangup/mute buttons, duration timer, keyboard input support
- Backend: POST /api/twilio/voice-token issues Access Token for browser
  Device; POST /api/twilio/webhook/voice-browser is the TwiML webhook
  Twilio calls to bridge the browser to the patient's phone
- TwiML App SID field added to Twilio Settings (stored in templates JSON)
- README: one-time Twilio Console setup instructions for the dial pad

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-02 22:24:53 -04:00
co-authored by Claude Sonnet 4.6
parent be90966f6e
commit ddcc49b72c
9 changed files with 409 additions and 4 deletions
@@ -1203,4 +1203,26 @@ router.post("/webhook/voice-recording", async (req: Request, res: Response): Pro
}
});
// POST /api/twilio/webhook/voice-browser
// Called by Twilio when a browser Device (Voice SDK) places an outbound call.
// Looks up the office's Twilio phone number by AccountSid and dials the patient.
router.post("/webhook/voice-browser", async (req: Request, res: Response): Promise<any> => {
try {
const { To, AccountSid } = req.body as { To?: string; AccountSid?: string };
if (!To || !AccountSid) {
res.set("Content-Type", "text/xml");
return res.send("<Response><Say>Missing call parameters.</Say></Response>");
}
const settings = await db.twilioSettings.findFirst({ where: { accountSid: AccountSid } });
const callerId = settings?.phoneNumber || "";
res.set("Content-Type", "text/xml");
return res.send(
`<Response><Dial callerId="${escapeXml(callerId)}"><Number>${escapeXml(To)}</Number></Dial></Response>`
);
} catch {
res.set("Content-Type", "text/xml");
return res.send("<Response><Say>An error occurred. Please try again.</Say></Response>");
}
});
export default router;