/** * Admin Panel: Communications Configuration Console * (GET/POST /admin/communications-config*). * * Covers the real channels this repo has code for -- Email (two * previously-parallel raw-SMTP scripts, now sharing credential * resolution via email_adapter.py) and WhatsApp (whatsapp_client.py, * genuinely wired into the candidate-decision notification workflow) -- * plus the voice interview bot (voicebot/voice_bot.py), kept as a * SEPARATE lifecycle from communications per the spec's explicit "keep * bot config separate" rule. SMS is honestly labeled absent: there is * no SMS provider integration anywhere in this repository. * * Test messaging always requires an explicit recipient and confirmation, * and is clearly labeled as a test in the message itself. WhatsApp has * no safe test-send path (Meta's template-approval rules), so only a * real connectivity check (no message) is offered for it. */ function CommunicationsConfig(props) { var api = props.api; var permissions = props.permissions || []; var tabState = React.useState("overview"); var tab = tabState[0]; var setTab = tabState[1]; var canWrite = permissions.indexOf("comms.config.write") !== -1; var canActivate = permissions.indexOf("comms.config.activate") !== -1; var canTest = permissions.indexOf("comms.test.send") !== -1; var canManageTemplates = permissions.indexOf("comms.templates.manage") !== -1; return (

Communications Configuration

Configures the real email and WhatsApp send paths, the voice interview bot, message templates, and shared delivery policies this app actually uses. SMS is labeled "absent" below, honestly -- there is no SMS provider code anywhere in this repository.

{window.CDPI_CONFIG.mode === "mock" && (
Mock mode: no real SMTP server or WhatsApp Graph API access exists in this browser-only demo -- every connectivity check honestly reports "unknown" and no test email is actually sent. Switch config.js to mode:"real" against a deployed backend to exercise the real adapters.
)}
{tab === "overview" && } {tab === "email" && } {tab === "whatsapp" && } {tab === "sms" && } {tab === "bot" && } {tab === "templates" && } {tab === "policies" && } {tab === "history" && }
); } function outcomeBadgeClass(outcome) { if (outcome === "healthy") return "badge-good"; if (outcome === "unhealthy") return "badge-bad"; return "badge-neutral"; } function CommsOverviewTab(props) { var api = props.api; var snapshot = CDPI_REPORT_SNAPSHOT.useReportSnapshot("comms_config_overview", function () { return api.getCommsConfig(); }); var data = { loading: snapshot.state.loading && !snapshot.state.data, error: snapshot.state.error, data: snapshot.state.data }; var load = snapshot.refresh; if (data.error && !data.data) return
{data.error}
; if (data.loading) return

Loading…

; return (
{data.data.channels.map(function (c) { return (
{c.category}
{c.status}

{c.description}

); })}

Active configuration (version {data.data.active.config_version})

{data.data.active.note &&

{data.data.active.note}

}
Email{data.data.active.email.enabled ? "enabled" : "disabled"} -- sender "{data.data.active.email.sender_display_name}"
WhatsApp{data.data.active.whatsapp.enabled ? "enabled" : "disabled"}
Duplicate-send window{data.data.active.delivery_policies.duplicate_send_window_minutes} minutes
{data.data.draft &&
A draft configuration (version {data.data.draft.config_version}) is pending activation.
}
); } function CommsEmailTab(props) { var api = props.api; var canTest = props.canTest; var connState = React.useState({ busy: false, result: null, error: null }); var conn = connState[0]; var setConn = connState[1]; var sendState = React.useState({ to: "", busy: false, result: null, error: null }); var send = sendState[0]; var setSend = sendState[1]; function handleTestConnectivity() { setConn({ busy: true, result: null, error: null }); api.testEmailConnectivity() .then(function (result) { setConn({ busy: false, result: result, error: null }); }) .catch(function (err) { setConn({ busy: false, result: null, error: err.message }); }); } function handleSendTest() { if (!send.to.trim()) return; if (!window.confirm("Send a real TEST email to " + send.to + "? It will be clearly labeled as a test message.")) return; setSend(function (s) { return Object.assign({}, s, { busy: true, result: null, error: null }); }); api.sendTestEmail(send.to.trim(), "Communications Configuration test", "") .then(function (result) { setSend(function (s) { return Object.assign({}, s, { busy: false, result: result, error: null }); }); }) .catch(function (err) { setSend(function (s) { return Object.assign({}, s, { busy: false, result: null, error: err.message }); }); }); } return (

Raw SMTP via smtplib (email_adapter.py). Credentials resolve from your .env file/Key Vault first, falling back to the legacy per-script JSON config files if unset -- never shown or editable here.

{canTest && (
{conn.error &&
{conn.error}
} {conn.result && (
{conn.result.outcome} {conn.result.detail}
)}

Send a test email

Requires an explicit recipient and confirmation. The message is always clearly labeled as a test.

{send.error &&
{send.error}
} {send.result &&
Test email sent to {send.result.to}.
}
)} {!canTest &&

Your account doesn't have the 'comms.test.send' permission.

}
); } function CommsWhatsappTab(props) { var api = props.api; var canTest = props.canTest; var connState = React.useState({ busy: false, result: null, error: null }); var conn = connState[0]; var setConn = connState[1]; function handleTestConnectivity() { setConn({ busy: true, result: null, error: null }); api.testWhatsappConnectivity() .then(function (result) { setConn({ busy: false, result: result, error: null }); }) .catch(function (err) { setConn({ busy: false, result: null, error: err.message }); }); } return (

Meta WhatsApp Business Cloud API (whatsapp_client.py) -- genuinely wired into the candidate-decision notification workflow. Templates are authored and approved in Meta Business Manager, not here; this console only maps a workflow to an approved template name (see the Delivery Policies tab).

No test-message send is offered for WhatsApp -- Meta's template-approval rules mean there is no safe sandbox send path. Only a real connectivity check (metadata lookup, no message) is available.
{canTest && (
{conn.error &&
{conn.error}
} {conn.result && (
{conn.result.outcome} {conn.result.detail}
)}
)}
); } function CommsSmsTab() { return (

SMS is absent -- there is no SMS provider integration anywhere in this repository (no Twilio, no SNS, no provider client of any kind, not even an unprovisioned stub). Adding SMS support requires real development work: choosing a provider, provisioning an account, and writing a client module, none of which exists yet.

); } function CommsInterviewBotTab(props) { var api = props.api; var canWrite = props.canWrite; var canActivate = props.canActivate; var state = React.useState({ loading: true, error: null, active: null, draft: null }); var data = state[0]; var setData = state[1]; var formState = React.useState(null); var form = formState[0]; var setForm = formState[1]; var saveState = React.useState({ saving: false, error: null }); var save = saveState[0]; var setSave = saveState[1]; function load() { setData(function (d) { return Object.assign({}, d, { loading: true, error: null }); }); api.getInterviewBotConfig() .then(function (result) { setData({ loading: false, error: null, active: result.active, draft: result.draft }); setForm(result.draft || result.active); }) .catch(function (err) { setData(function (d) { return Object.assign({}, d, { loading: false, error: err.message }); }); }); } React.useEffect(function () { load(); }, []); function setField(key, value) { setForm(function (f) { return Object.assign({}, f, { [key]: value }); }); } function handleSaveDraft() { setSave({ saving: true, error: null }); api.saveInterviewBotConfigDraft(form) .then(function () { setSave({ saving: false, error: null }); load(); }) .catch(function (err) { setSave({ saving: false, error: err.message }); }); } function handleActivate() { if (!window.confirm("Activate this interview bot configuration? It will replace what voice_bot.py reads on its next run.")) return; setSave({ saving: true, error: null }); api.activateInterviewBotConfig() .then(function () { setSave({ saving: false, error: null }); load(); }) .catch(function (err) { setSave({ saving: false, error: err.message }); }); } if (data.error) return
{data.error}
; if (data.loading || !form) return

Loading…

; return (

voicebot/voice_bot.py's real, tunable parameters -- kept as a SEPARATE configuration from the communications channels above, per the spec's "keep bot config separate from communication configuration" rule. Only real fields are exposed: no arbitrary system-prompt text or code.

{data.active.note &&

{data.active.note}

}

No duration limit, retry policy, or voice settings beyond the above are exposed -- voice_bot.py has no code path for them today (English-only, no configurable timeout). Listing them would misrepresent what the bot can actually do.

{canWrite && (
{canActivate && data.draft && }
)} {save.error &&
{save.error}
}
); } function CommsTemplatesTab(props) { var api = props.api; var canManageTemplates = props.canManageTemplates; var state = React.useState({ loading: true, error: null, templates: null }); var data = state[0]; var setData = state[1]; var editState = React.useState({}); var edits = editState[0]; var setEdits = editState[1]; var previewState = React.useState({}); var previews = previewState[0]; var setPreviews = previewState[1]; var actionState = React.useState({ busy: false, error: null }); var action = actionState[0]; var setAction = actionState[1]; function load() { setData(function (d) { return Object.assign({}, d, { loading: true, error: null }); }); api.listCommsTemplates() .then(function (result) { setData({ loading: false, error: null, templates: result.templates }); }) .catch(function (err) { setData(function (d) { return Object.assign({}, d, { loading: false, error: err.message }); }); }); } React.useEffect(function () { load(); }, []); function handlePreview(templateId) { api.previewCommsTemplate(templateId) .then(function (result) { setPreviews(function (p) { return Object.assign({}, p, { [templateId]: result }); }); }) .catch(function (err) { setAction({ busy: false, error: err.message }); }); } function handleCreateDraft(templateId) { var e = edits[templateId] || {}; if (!e.body) return; setAction({ busy: true, error: null }); api.createCommsTemplateDraft(templateId, e.subject || "", e.body) .then(function () { setAction({ busy: false, error: null }); setEdits(function (ed) { return Object.assign({}, ed, { [templateId]: undefined }); }); load(); }) .catch(function (err) { setAction({ busy: false, error: err.message }); }); } function handleActivateVersion(templateId, version) { if (!window.confirm("Activate template version " + version + "?")) return; setAction({ busy: true, error: null }); api.activateCommsTemplateVersion(templateId, version) .then(function () { setAction({ busy: false, error: null }); load(); }) .catch(function (err) { setAction({ busy: false, error: err.message }); }); } if (data.error) return
{data.error}
; if (data.loading) return

Loading…

; return (

Reusable, versioned templates -- editing and activating one here changes what the real send path uses next. Previews always use clearly-labeled SYNTHETIC example data, never a real candidate's details.

{action.error &&
{action.error}
} {Object.keys(data.templates).map(function (templateId) { var versions = data.templates[templateId].slice().sort(function (a, b) { return b.version - a.version; }); var active = versions.filter(function (v) { return v.status === "active"; })[0]; var edit = edits[templateId] || { subject: active ? active.subject : "", body: active ? active.body : "" }; var preview = previews[templateId]; return (

{templateId}

Active: version {active ? active.version : "none"}

{versions.map(function (v) { return ( ); })}
VersionStatusCreated by
{v.version} {v.status} {v.created_by} {canManageTemplates && v.status !== "active" && }
{preview && (

Synthetic example data -- never real candidate information.

{preview.rendered_subject &&

Subject: {preview.rendered_subject}

}
{preview.rendered_body}
)} {canManageTemplates && (