← back to Butlr
wizard step 2: DNC pre-check on phone field + directory presets
0dd1395a649b894d82aefb4f0baddf1a212d99cd · 2026-05-13 10:19:46 -0700 · SteveStudio2
views/public/wizard-2-business.ejs:
- Custom-phone input fires /api/dnc-check on blur — shows 🚫 + red
explainer + disables "Next" button if number is on DNC.
- Selecting a directory preset radio also pre-checks (pulls phone from
the muted sibling line). Catches the case where Steve's curated
businesses.json includes a number that's been added to suppression.
- ✓ "not on DNC" or "(dry-run mode)" confirmation on allowed.
Same contract as the /calls quick-dial form (deployed prior). Now
users see DNC status BEFORE submitting either dial path.
Files touched
M views/public/wizard-2-business.ejs
Diff
commit 0dd1395a649b894d82aefb4f0baddf1a212d99cd
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 10:19:46 2026 -0700
wizard step 2: DNC pre-check on phone field + directory presets
views/public/wizard-2-business.ejs:
- Custom-phone input fires /api/dnc-check on blur — shows 🚫 + red
explainer + disables "Next" button if number is on DNC.
- Selecting a directory preset radio also pre-checks (pulls phone from
the muted sibling line). Catches the case where Steve's curated
businesses.json includes a number that's been added to suppression.
- ✓ "not on DNC" or "(dry-run mode)" confirmation on allowed.
Same contract as the /calls quick-dial form (deployed prior). Now
users see DNC status BEFORE submitting either dial path.
---
views/public/wizard-2-business.ejs | 55 ++++++++++++++++++++++++++++++++++++--
1 file changed, 53 insertions(+), 2 deletions(-)
diff --git a/views/public/wizard-2-business.ejs b/views/public/wizard-2-business.ejs
index 6dc3bcf..6a2b05f 100644
--- a/views/public/wizard-2-business.ejs
+++ b/views/public/wizard-2-business.ejs
@@ -47,12 +47,13 @@
</label>
<label class="field">
<span class="field-label">Phone number to call</span>
- <input type="tel" name="business_phone" value="<%= state.business_phone || '' %>" maxlength="20" placeholder="1-800-...">
+ <input type="tel" id="wizard-business-phone" name="business_phone" value="<%= state.business_phone || '' %>" maxlength="20" placeholder="1-800-...">
+ <span id="wizard-dnc-status" class="muted small" style="display:block;font-size:11px;min-height:14px;margin-top:4px;"></span>
</label>
</fieldset>
<div class="form-actions">
- <button type="submit" class="btn primary">Next: what you want done →</button>
+ <button id="wizard-next-btn" type="submit" class="btn primary">Next: what you want done →</button>
<button type="submit" formmethod="POST" formaction="/new" formnovalidate class="btn ghost">← Back</button>
</div>
</form>
@@ -62,3 +63,53 @@
<%- include('../partials/footer') %>
<script src="/js/wizard.js" defer></script>
+<script>
+(function(){
+ // DNC pre-check on phone field — same contract as /calls quick-dial.
+ // Fires on blur of the custom-phone input OR when the user picks a
+ // directory preset (the radio button has a sibling muted "phone" line).
+ const phoneEl = document.getElementById('wizard-business-phone');
+ const stat = document.getElementById('wizard-dnc-status');
+ const nextBtn = document.getElementById('wizard-next-btn');
+ if (!phoneEl || !stat || !nextBtn) return;
+
+ async function check(num) {
+ if (!num) { stat.textContent = ''; nextBtn.disabled = false; return; }
+ stat.textContent = 'checking DNC…';
+ stat.style.color = 'var(--ink-dim)';
+ try {
+ const r = await fetch('/api/dnc-check?phone=' + encodeURIComponent(num));
+ const j = await r.json();
+ if (j.allowed) {
+ stat.textContent = j.dry_run_bypass ? '✓ ok (dry-run mode)' : '✓ not on DNC';
+ stat.style.color = 'var(--accent,#1a56db)';
+ nextBtn.disabled = false;
+ } else {
+ const msg = j.reason === 'federal_dnc' ? 'on federal DNC — Butlr will not dial this number'
+ : j.reason === 'internal_suppression' ? 'in internal suppression — Butlr will not dial this number'
+ : j.reason === 'gate_error' ? 'DNC list unavailable — fail-closed; cannot dial right now'
+ : 'blocked: ' + (j.reason || 'unknown');
+ stat.textContent = '🚫 ' + msg;
+ stat.style.color = '#991b1b';
+ nextBtn.disabled = true;
+ }
+ } catch (e) {
+ stat.textContent = '(DNC check failed; will re-check on submit)';
+ stat.style.color = 'var(--ink-dim)';
+ nextBtn.disabled = false;
+ }
+ }
+
+ phoneEl.addEventListener('blur', () => check(phoneEl.value.trim()));
+
+ // Also check when a preset is selected — pull the phone from the radio's
+ // sibling muted line and trigger.
+ document.querySelectorAll('input[name="business_slug"]').forEach(r => {
+ r.addEventListener('change', () => {
+ if (r.value === '__custom__') return; // custom uses the input field
+ const phoneLine = r.parentElement.querySelector('.business-pick-body .muted.small');
+ if (phoneLine) check(phoneLine.textContent.trim());
+ });
+ });
+})();
+</script>
← 73c5692 dnc: pre-check endpoint + /calls quick-dial inline warning
·
back to Butlr
·
cost-tracker: log Vapi voice_min + Twilio voice_min + SMS to eb1df5e →