-
Next action
-
{onboardingConfidence.nextAction}
-
{nextStep.title}
-
-
Test SMS {testSmsTruth.label}
-
{onboardingConfidence.readinessLabel}
- {!missedCallValidation.countsAsLaunchProof ?
Missed-call proof missing : null}
- {onboardingConfidence.state === 'live_with_warnings' ?
Live with warnings : null}
- {lastIssue.state === 'issue' ?
Needs attention : null}
+
+
+
Next action
+
{cardState.nextActionLabel}
+
+ {cardState.nextActionLabel === 'No action needed'
+ ? 'This business has no active operator blockers right now.'
+ : 'Open the exact remediation step from the business control panel.'}
+
{business.twilioWebhookSyncedAt ? (
Webhook sync {formatRelativeTime(business.twilioWebhookSyncedAt)}
- ) : (
+ ) : !cardState.isArchived ? (
Webhook sync still needed
- )}
- {missedCallValidation.verifiedAt ? (
-
Missed-call proof {formatRelativeTime(missedCallValidation.verifiedAt)}
) : null}
- {lastIssue.createdAt ?
Latest issue recorded {formatRelativeTime(lastIssue.createdAt)}
: null}
+ {lastIssue.createdAt && !cardState.isArchived ? (
+
Latest issue recorded {formatRelativeTime(lastIssue.createdAt)}
+ ) : null}
+ {cardState.nextActionLabel !== 'No action needed' ? (
+
+ {cardState.nextActionLabel}
+
+ ) : null}
Open business
- {blockerStepHref ? (
-
- Open blocker step
-
- ) : null}
Open customer workspace
@@ -1072,16 +1031,7 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
View support workspace snapshot
- {!isBusinessArchived(business) ? (
-
- ) : null}
- {assignedNumber ? (
+ {assignedNumber && !cardState.isArchived ? (
) : null}
- {isBusinessArchived(business) ? 'Restore / manage' : 'Archive / manage'}
+ {cardState.isArchived ? 'Restore business' : 'Archive business'}
diff --git a/lib/admin-dashboard.ts b/lib/admin-dashboard.ts
index 21bf910..1dd2218 100644
--- a/lib/admin-dashboard.ts
+++ b/lib/admin-dashboard.ts
@@ -15,9 +15,11 @@ import {
import type { TwilioWebhookSnapshot } from '@/lib/admin-provisioning-presenters';
import { DEMO_OWNER_CLERK_ID, isTestDemoBusiness } from '@/lib/admin-test-data-reset';
import type { AdminMissedCallValidationTruth } from '@/lib/admin-operator-proof';
+import type { AdminBusinessIssue, AdminTestSmsTruth } from '@/lib/admin-operator-visibility';
import { getBusinessPhoneSetupGate, getPublicBusinessPhone } from '@/lib/business-phone-setup';
-import { getManagedTextingNumber, getManagedTwilioStatusSummary } from '@/lib/managed-twilio-status';
+import { getManagedTextingNumber, getManagedTwilioStatusSummary, type ManagedTwilioSummary } from '@/lib/managed-twilio-status';
import { formatMessageStatus, isMessageDeliveryIssueStatus } from '@/lib/lead-presenters';
+import type { TwilioSetupStepKey } from '@/lib/twilio-setup';
type DashboardBusiness = Pick<
Business,
@@ -159,6 +161,46 @@ export type AdminOnboardingConfidence = {
hasRecentFailures: boolean;
};
+export type AdminBusinessPrimaryState =
+ | 'archived'
+ | 'paused'
+ | 'provisioning_failed'
+ | 'owner_blocked'
+ | 'compliance_pending'
+ | 'needs_attention'
+ | 'ready_for_test'
+ | 'ready_for_live'
+ | 'live_with_warnings'
+ | 'live'
+ | 'draft'
+ | 'in_setup';
+
+export type AdminBusinessCardBadge = {
+ key: string;
+ label: string;
+ variant: 'success' | 'secondary' | 'outline' | 'destructive';
+};
+
+export type AdminBusinessCardState = {
+ primaryState: AdminBusinessPrimaryState;
+ primaryLabel: string;
+ primaryVariant: 'success' | 'secondary' | 'outline' | 'destructive';
+ primaryReason: string;
+ nextActionLabel: string;
+ nextActionStepKey: TwilioSetupStepKey | null;
+ badges: AdminBusinessCardBadge[];
+ shouldAppearInNeedsAttention: boolean;
+ shouldAppearInPendingCompliance: boolean;
+ isArchived: boolean;
+ isLive: boolean;
+ isLiveWithWarnings: boolean;
+ isReadyForTest: boolean;
+ isReadyForLive: boolean;
+ isDraft: boolean;
+ isInSetup: boolean;
+ isPaused: boolean;
+};
+
export type AdminTestSmsConfidenceState = 'not_started' | 'pending_delivery' | 'delivered' | 'failed';
export const adminBoardFilterOptions: AdminBoardFilterOption[] = [
@@ -272,7 +314,7 @@ export function buildAdminNextStep(params: {
title: 'Provisioning needs attention',
detail: business.provisioningError,
tone: 'attention',
- actionLabel: hasTextingNumber ? 'Re-run provisioning' : 'Finish provisioning',
+ actionLabel: 'Fix provisioning',
};
}
@@ -294,7 +336,7 @@ export function buildAdminNextStep(params: {
: 'The business is saved, but the owner account still needs a deliberate admin action. Use Invite owner by email or Connect existing owner.'
: 'Add the owner email first, then choose Invite owner by email or Connect existing owner.',
tone: 'attention',
- actionLabel: 'Review owner setup',
+ actionLabel: 'Invite or connect owner',
};
}
@@ -306,7 +348,7 @@ export function buildAdminNextStep(params: {
? 'This business is set to use the main Twilio account directly. Confirm the parent-account mapping before continuing.'
: 'Managed provisioning cannot finish until the business has a Twilio subaccount attached.',
tone: 'attention',
- actionLabel: 'Re-run provisioning',
+ actionLabel: 'Fix provisioning',
};
}
@@ -315,7 +357,7 @@ export function buildAdminNextStep(params: {
title: 'No texting number is assigned',
detail: 'Provision a new number or attach the approved existing number so missed-call SMS can start.',
tone: 'attention',
- actionLabel: 'Provision number',
+ actionLabel: 'Fix provisioning',
};
}
@@ -324,7 +366,7 @@ export function buildAdminNextStep(params: {
title: 'Messaging service is missing',
detail: 'The business has a number, but Twilio messaging still needs a Messaging Service for compliant delivery.',
tone: 'attention',
- actionLabel: 'Re-run provisioning',
+ actionLabel: 'Fix provisioning',
};
}
@@ -351,7 +393,7 @@ export function buildAdminNextStep(params: {
title: 'Toll-free verification still needs recording',
detail: 'Messaging is wired up, but toll-free verification still needs to be recorded before compliant live texting can launch.',
tone: 'pending',
- actionLabel: 'Review toll-free verification',
+ actionLabel: 'Open toll-free verification',
};
}
@@ -360,7 +402,7 @@ export function buildAdminNextStep(params: {
title: 'Business verification still needed',
detail: 'Messaging is wired up, but the A2P business details still need to be completed before compliant live texting can launch.',
tone: 'pending',
- actionLabel: 'Review A2P readiness',
+ actionLabel: 'Open A2P readiness',
};
}
@@ -396,7 +438,7 @@ export function buildAdminNextStep(params: {
title: 'Compliance review needs attention',
detail: business.a2pFailureReason || business.tollFreeVerificationNote || managedSummary.nextStep,
tone: 'attention',
- actionLabel: 'Review compliance notes',
+ actionLabel: managedSummary.complianceType === MessagingComplianceType.TOLL_FREE_VERIFICATION ? 'Open toll-free verification' : 'Fix compliance',
};
}
@@ -405,16 +447,16 @@ export function buildAdminNextStep(params: {
title: 'Ready to go live',
detail: 'Infrastructure and compliance look ready. Mark the business live when you want automation active.',
tone: 'pending',
- actionLabel: 'Mark live',
+ actionLabel: 'Mark business live',
};
}
if (managedSummary.messagingReady && business.provisioningStatus === BusinessProvisioningStatus.LIVE) {
return {
- title: 'Business is live and healthy',
- detail: 'Twilio, webhooks, alerts, and rollout status all look ready for normal operator monitoring.',
+ title: 'Business is live',
+ detail: 'Core Twilio, webhook, alert, and rollout setup is active. Use the proof and issue signals to decide whether any follow-up is still needed.',
tone: 'healthy',
- actionLabel: 'Open support workspace',
+ actionLabel: 'No action needed',
};
}
@@ -693,7 +735,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'outline',
readinessLabel: 'Not ready',
readinessVariant: 'outline',
- nextAction: nextStep.title,
+ nextAction: nextStep.actionLabel,
summary: 'Core onboarding details are still missing. Start with owner connection and Twilio setup.',
},
in_setup: {
@@ -701,7 +743,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'secondary',
readinessLabel: 'Not ready',
readinessVariant: 'outline',
- nextAction: nextStep.title,
+ nextAction: nextStep.actionLabel,
summary: 'Onboarding is in progress, but at least one blocking setup step still needs attention.',
},
needs_attention: {
@@ -709,7 +751,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'destructive',
readinessLabel: 'Not ready',
readinessVariant: 'destructive',
- nextAction: nextStep.title,
+ nextAction: nextStep.actionLabel,
summary: 'Something is broken or incomplete enough that the founder should stop and repair it before testing.',
},
waiting_on_a2p: {
@@ -731,7 +773,7 @@ export function buildAdminOnboardingConfidence(params: {
nextAction: hasTestSmsSuccess
? 'Run a real missed-call test'
: hasPendingTestSmsDelivery
- ? 'Confirm test SMS delivery'
+ ? 'Send test SMS'
: 'Send a test SMS',
summary: 'The business looks ready for operator-led validation. Run the checks before you mark it live.',
},
@@ -740,7 +782,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'success',
readinessLabel: 'Ready for live',
readinessVariant: 'success',
- nextAction: 'Mark this business live',
+ nextAction: 'Mark business live',
summary: 'Setup, compliance, and operator validation checks are in place for a clean launch decision.',
},
live: {
@@ -748,7 +790,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'success',
readinessLabel: 'Ready for live',
readinessVariant: 'success',
- nextAction: 'Monitor normal activity',
+ nextAction: 'No action needed',
summary: 'This business is live and the operator confidence checks are green.',
},
live_with_warnings: {
@@ -756,7 +798,7 @@ export function buildAdminOnboardingConfidence(params: {
stateVariant: 'destructive',
readinessLabel: 'Live with warnings',
readinessVariant: 'destructive',
- nextAction: nextStep.title,
+ nextAction: nextStep.actionLabel,
summary: 'The business is live, but recent failures or missing validations mean the founder should review it closely.',
},
archived: {
@@ -781,49 +823,459 @@ export function buildAdminOnboardingConfidence(params: {
} satisfies AdminOnboardingConfidence;
}
-export function matchesAdminBoardFilter(
- business: DashboardBusiness,
- notificationSettings: DashboardNotificationSettings | null,
- ownerConnected: boolean,
- filter: AdminBoardFilter
+function buildSecondaryBadge(
+ key: string,
+ label: string,
+ variant: AdminBusinessCardBadge['variant']
) {
- if (filter === 'all') return true;
+ return { key, label, variant } satisfies AdminBusinessCardBadge;
+}
- const managedSummary = getManagedTwilioStatusSummary(business);
- const nextStep = buildAdminNextStep({ business, notificationSettings, ownerConnected });
- const archived = isBusinessArchived(business);
- const paused = isBusinessAutomationPaused(business) && !archived;
+function dedupeAdminBusinessCardBadges(badges: AdminBusinessCardBadge[], limit = 3) {
+ const seen = new Set
();
+ const deduped: AdminBusinessCardBadge[] = [];
+
+ for (const badge of badges) {
+ const signature = `${badge.key}:${badge.label}`;
+ if (seen.has(signature)) continue;
+ seen.add(signature);
+ deduped.push(badge);
+ if (deduped.length >= limit) break;
+ }
+
+ return deduped;
+}
- if (filter === 'archived') return archived;
- if (archived) return false;
+function getTestSmsBadge(testSmsTruth: AdminTestSmsTruth) {
+ if (testSmsTruth.state === 'delivered') {
+ return buildSecondaryBadge('test_sms', 'Test SMS Delivered', 'success');
+ }
+
+ if (testSmsTruth.state === 'failed') {
+ return buildSecondaryBadge('test_sms', 'Test SMS Failed', 'destructive');
+ }
+
+ if (testSmsTruth.state === 'pending') {
+ return buildSecondaryBadge('test_sms', 'Test SMS Pending', 'outline');
+ }
+
+ return buildSecondaryBadge('test_sms', 'Test SMS Not Run', 'outline');
+}
+
+function getComplianceBadge(params: {
+ managedSummary: ManagedTwilioSummary;
+ business: Pick;
+}) {
+ if (params.managedSummary.complianceTypeUnknown) {
+ return buildSecondaryBadge('compliance', 'Compliance Not Selected', 'outline');
+ }
+
+ if (params.managedSummary.attentionRequired) {
+ return buildSecondaryBadge('compliance', 'Compliance Needs Attention', 'destructive');
+ }
+
+ if (
+ params.managedSummary.compliancePendingReview ||
+ (params.managedSummary.complianceType === MessagingComplianceType.TOLL_FREE_VERIFICATION && !params.managedSummary.complianceStarted) ||
+ (params.managedSummary.complianceType === MessagingComplianceType.LOCAL_A2P &&
+ params.business.managedTwilioStatus === ManagedTwilioStatus.AWAITING_BUSINESS_VERIFICATION)
+ ) {
+ return buildSecondaryBadge('compliance', 'Compliance Pending', 'outline');
+ }
- if (filter === 'paused') {
- return paused || business.managedTwilioStatus === ManagedTwilioStatus.PAUSED_NONCOMPLIANT;
+ return null;
+}
+
+function getProvisioningRepairStepKey(
+ business: DashboardBusiness,
+ managedSummary: ManagedTwilioSummary
+): TwilioSetupStepKey {
+ if (!managedSummary.accountReady) return 'account_ready';
+ if (!getManagedTextingNumber(business) || !(business.twilioPrimaryNumberSid || business.twilioPhoneNumberSid)) return 'number_assigned';
+ if (!business.twilioMessagingServiceSid) return 'messaging_service_ready';
+ if (!business.twilioWebhookSyncedAt) return 'voice_webhook_synced';
+ return 'account_ready';
+}
+
+function getAttentionReasonAndStep(params: {
+ business: DashboardBusiness;
+ managedSummary: ManagedTwilioSummary;
+ nextStep: AdminNextStep;
+ onboardingConfidence: AdminOnboardingConfidence;
+ lastIssue: AdminBusinessIssue;
+ testSmsTruth: AdminTestSmsTruth;
+ missedCallValidation: AdminMissedCallValidationTruth;
+}) {
+ const { business, managedSummary, nextStep, onboardingConfidence, lastIssue, testSmsTruth, missedCallValidation } = params;
+ const hasAssignedNumber = Boolean(getManagedTextingNumber(business) && (business.twilioPrimaryNumberSid || business.twilioPhoneNumberSid));
+
+ if (lastIssue.eventType === 'webhooks.sync_failed' || (hasAssignedNumber && !business.twilioWebhookSyncedAt)) {
+ return {
+ primaryState: 'needs_attention' as const,
+ primaryLabel: 'Needs attention',
+ primaryVariant: 'destructive' as const,
+ primaryReason: 'Twilio webhooks are not synced to the current CallbackCloser URLs.',
+ nextActionLabel: 'Re-sync webhooks',
+ nextActionStepKey: 'voice_webhook_synced' as const,
+ };
}
- if (filter === 'live') {
- return business.provisioningStatus === BusinessProvisioningStatus.LIVE && managedSummary.messagingReady;
+ if (testSmsTruth.state === 'failed') {
+ return {
+ primaryState: 'needs_attention' as const,
+ primaryLabel: 'Needs attention',
+ primaryVariant: 'destructive' as const,
+ primaryReason: 'The latest test SMS did not complete cleanly.',
+ nextActionLabel: 'Send test SMS',
+ nextActionStepKey: 'test_sms_delivered' as const,
+ };
}
- if (filter === 'pending_a2p') {
- return managedSummary.compliancePendingReview;
+ if (onboardingConfidence.readyForTest && testSmsTruth.state !== 'delivered') {
+ return {
+ primaryState: 'ready_for_test' as const,
+ primaryLabel: 'Ready for test',
+ primaryVariant: 'secondary' as const,
+ primaryReason:
+ testSmsTruth.state === 'pending'
+ ? 'The latest test SMS is still waiting on delivery confirmation.'
+ : 'A delivered test SMS is still missing.',
+ nextActionLabel: 'Send test SMS',
+ nextActionStepKey: 'test_sms_delivered' as const,
+ };
}
- if (filter === 'pending_setup') {
- return business.provisioningStatus === BusinessProvisioningStatus.DRAFT;
+ if (onboardingConfidence.readyForTest && !missedCallValidation.countsAsLaunchProof) {
+ return {
+ primaryState: 'ready_for_test' as const,
+ primaryLabel: 'Ready for test',
+ primaryVariant: 'secondary' as const,
+ primaryReason: 'The missed-call flow has not been fully validated yet.',
+ nextActionLabel: 'Validate missed-call flow',
+ nextActionStepKey: 'missed_call_validated' as const,
+ };
}
- if (filter === 'in_setup') {
- return business.provisioningStatus === BusinessProvisioningStatus.ONBOARDING || (!managedSummary.onboardingReady && ownerConnected);
+ return {
+ primaryState: nextStep.tone === 'attention' ? ('needs_attention' as const) : ('in_setup' as const),
+ primaryLabel: nextStep.tone === 'attention' ? 'Needs attention' : 'In setup',
+ primaryVariant: nextStep.tone === 'attention' ? ('destructive' as const) : ('secondary' as const),
+ primaryReason: nextStep.detail,
+ nextActionLabel: nextStep.actionLabel || 'Open blocker step',
+ nextActionStepKey: lastIssue.remediationStepKey,
+ };
+}
+
+export function buildAdminBusinessCardState(params: {
+ business: DashboardBusiness;
+ notificationSettings: DashboardNotificationSettings | null;
+ ownerConnected: boolean;
+ nextStep: AdminNextStep;
+ managedSummary: ManagedTwilioSummary;
+ onboardingConfidence: AdminOnboardingConfidence;
+ lastIssue: AdminBusinessIssue;
+ testSmsTruth: AdminTestSmsTruth;
+ missedCallValidation: AdminMissedCallValidationTruth;
+}) {
+ const { business, notificationSettings, ownerConnected, nextStep, managedSummary, onboardingConfidence, lastIssue, testSmsTruth, missedCallValidation } =
+ params;
+ const archived = isBusinessArchived(business);
+ const paused = !archived && business.provisioningStatus === BusinessProvisioningStatus.PAUSED;
+ const ownerEmail = notificationSettings?.ownerEmail?.trim() || null;
+ const ownerPhone = notificationSettings?.ownerPhone?.trim() || business.notifyPhone || null;
+ const missingOwnerContact = !ownerEmail && !ownerPhone;
+ const needsOwnerConnection = !ownerConnected;
+ const complianceNeedsOperatorAction =
+ managedSummary.attentionRequired ||
+ managedSummary.complianceTypeUnknown ||
+ (managedSummary.complianceType === MessagingComplianceType.TOLL_FREE_VERIFICATION && !managedSummary.complianceStarted) ||
+ (managedSummary.complianceType === MessagingComplianceType.LOCAL_A2P &&
+ business.managedTwilioStatus === ManagedTwilioStatus.AWAITING_BUSINESS_VERIFICATION);
+ const compliancePendingReview = managedSummary.compliancePendingReview;
+
+ let state: Pick<
+ AdminBusinessCardState,
+ 'primaryState' | 'primaryLabel' | 'primaryVariant' | 'primaryReason' | 'nextActionLabel' | 'nextActionStepKey'
+ >;
+
+ if (archived) {
+ state = {
+ primaryState: 'archived',
+ primaryLabel: 'Archived',
+ primaryVariant: 'outline',
+ primaryReason: 'Automation is paused because this business is archived.',
+ nextActionLabel: 'Restore business',
+ nextActionStepKey: null,
+ };
+ } else if (paused) {
+ state = {
+ primaryState: 'paused',
+ primaryLabel: 'Paused',
+ primaryVariant: 'outline',
+ primaryReason: 'Automation is paused until you intentionally resume this business.',
+ nextActionLabel: 'Resume automation',
+ nextActionStepKey: 'safe_to_mark_live',
+ };
+ } else if (business.provisioningError || business.provisioningStatus === BusinessProvisioningStatus.NEEDS_ATTENTION) {
+ state = {
+ primaryState: 'provisioning_failed',
+ primaryLabel: 'Provisioning failed',
+ primaryVariant: 'destructive',
+ primaryReason: business.provisioningError || 'The latest provisioning run needs operator repair before this business can move forward.',
+ nextActionLabel: 'Fix provisioning',
+ nextActionStepKey: getProvisioningRepairStepKey(business, managedSummary),
+ };
+ } else if (missingOwnerContact) {
+ state = {
+ primaryState: 'owner_blocked',
+ primaryLabel: 'Owner blocked',
+ primaryVariant: 'destructive',
+ primaryReason: 'Owner contact information is missing, so invites and alerts cannot be trusted yet.',
+ nextActionLabel: 'Save owner contact info',
+ nextActionStepKey: 'owner_connected',
+ };
+ } else if (needsOwnerConnection) {
+ state = {
+ primaryState: 'owner_blocked',
+ primaryLabel: 'Owner blocked',
+ primaryVariant: 'destructive',
+ primaryReason: business.ownerInviteSentAt
+ ? 'The owner account is still not connected even though an invite was already sent.'
+ : 'The owner account still needs to be connected before this business can be trusted.',
+ nextActionLabel: 'Invite or connect owner',
+ nextActionStepKey: 'owner_connected',
+ };
+ } else if (business.provisioningStatus === BusinessProvisioningStatus.LIVE && onboardingConfidence.blockers.length > 0) {
+ state = testSmsTruth.state !== 'delivered'
+ ? {
+ primaryState: 'live_with_warnings',
+ primaryLabel: 'Live with warnings',
+ primaryVariant: 'destructive',
+ primaryReason:
+ testSmsTruth.state === 'pending'
+ ? 'The latest test SMS is still waiting on delivery confirmation.'
+ : 'The latest test SMS is not fully proven.',
+ nextActionLabel: 'Send test SMS',
+ nextActionStepKey: 'test_sms_delivered',
+ }
+ : !missedCallValidation.countsAsLaunchProof
+ ? {
+ primaryState: 'live_with_warnings',
+ primaryLabel: 'Live with warnings',
+ primaryVariant: 'destructive',
+ primaryReason: 'Missed-call flow has not been fully validated.',
+ nextActionLabel: 'Validate missed-call flow',
+ nextActionStepKey: 'missed_call_validated',
+ }
+ : {
+ primaryState: 'live_with_warnings',
+ primaryLabel: 'Live with warnings',
+ primaryVariant: 'destructive',
+ primaryReason: onboardingConfidence.blockers[0]?.message || 'This live business still has unresolved operator warnings.',
+ nextActionLabel: nextStep.actionLabel || 'Open blocker step',
+ nextActionStepKey: lastIssue.remediationStepKey,
+ };
+ } else if (business.provisioningStatus === BusinessProvisioningStatus.LIVE) {
+ state = {
+ primaryState: 'live',
+ primaryLabel: 'Live',
+ primaryVariant: 'success',
+ primaryReason: 'No active warnings are recorded for this live business.',
+ nextActionLabel: 'No action needed',
+ nextActionStepKey: null,
+ };
+ } else if (complianceNeedsOperatorAction || compliancePendingReview) {
+ state = {
+ primaryState: 'compliance_pending',
+ primaryLabel: managedSummary.attentionRequired ? 'Compliance blocked' : 'Pending compliance',
+ primaryVariant: managedSummary.attentionRequired ? 'destructive' : 'outline',
+ primaryReason: managedSummary.attentionRequired
+ ? business.a2pFailureReason || business.tollFreeVerificationNote || managedSummary.nextStep
+ : managedSummary.complianceTypeUnknown
+ ? 'Messaging compliance type has not been selected yet.'
+ : managedSummary.complianceType === MessagingComplianceType.TOLL_FREE_VERIFICATION && !managedSummary.complianceStarted
+ ? 'Toll-free verification has not been recorded yet.'
+ : managedSummary.nextStep,
+ nextActionLabel: managedSummary.complianceTypeUnknown
+ ? 'Choose number type'
+ : managedSummary.complianceType === MessagingComplianceType.TOLL_FREE_VERIFICATION
+ ? 'Open toll-free verification'
+ : managedSummary.attentionRequired
+ ? 'Fix compliance'
+ : 'Wait for approval',
+ nextActionStepKey: 'a2p_status_recorded',
+ };
+ } else if (onboardingConfidence.readyForLive) {
+ state = {
+ primaryState: 'ready_for_live',
+ primaryLabel: 'Ready for live',
+ primaryVariant: 'success',
+ primaryReason: 'Launch proof is in place and automation can go live safely.',
+ nextActionLabel: 'Mark business live',
+ nextActionStepKey: 'safe_to_mark_live',
+ };
+ } else if (onboardingConfidence.readyForTest) {
+ state = getAttentionReasonAndStep({
+ business,
+ managedSummary,
+ nextStep,
+ onboardingConfidence,
+ lastIssue,
+ testSmsTruth,
+ missedCallValidation,
+ });
+ } else if (onboardingConfidence.state === 'draft') {
+ state = {
+ primaryState: 'draft',
+ primaryLabel: 'Draft',
+ primaryVariant: 'outline',
+ primaryReason: onboardingConfidence.summary,
+ nextActionLabel: nextStep.actionLabel,
+ nextActionStepKey: lastIssue.remediationStepKey,
+ };
+ } else {
+ state = getAttentionReasonAndStep({
+ business,
+ managedSummary,
+ nextStep,
+ onboardingConfidence,
+ lastIssue,
+ testSmsTruth,
+ missedCallValidation,
+ });
}
- if (filter === 'needs_attention') {
- return nextStep.tone === 'attention';
+ const badges: AdminBusinessCardBadge[] = [];
+ badges.push(buildSecondaryBadge('customer_type', business.isTestBusiness ? 'Test' : 'Real customer', 'outline'));
+
+ if (!archived) {
+ if (
+ state.primaryState === 'ready_for_test' ||
+ state.primaryState === 'ready_for_live' ||
+ state.primaryState === 'live_with_warnings' ||
+ state.primaryState === 'live' ||
+ state.primaryState === 'needs_attention'
+ ) {
+ badges.push(getTestSmsBadge(testSmsTruth));
+ }
+
+ const complianceBadge = getComplianceBadge({ managedSummary, business });
+ if (complianceBadge) {
+ badges.push(complianceBadge);
+ }
+
+ if (
+ !missedCallValidation.countsAsLaunchProof &&
+ (state.primaryState === 'ready_for_test' ||
+ state.primaryState === 'ready_for_live' ||
+ state.primaryState === 'live_with_warnings' ||
+ state.primaryState === 'live')
+ ) {
+ badges.push(buildSecondaryBadge('missed_call_proof', 'Missed-call proof missing', 'outline'));
+ }
}
+ return {
+ ...state,
+ badges: dedupeAdminBusinessCardBadges(badges),
+ shouldAppearInNeedsAttention:
+ !archived &&
+ (state.primaryState === 'provisioning_failed' ||
+ state.primaryState === 'owner_blocked' ||
+ state.primaryState === 'needs_attention' ||
+ state.primaryState === 'live_with_warnings'),
+ shouldAppearInPendingCompliance: !archived && state.primaryState === 'compliance_pending',
+ isArchived: archived,
+ isLive: state.primaryState === 'live',
+ isLiveWithWarnings: state.primaryState === 'live_with_warnings',
+ isReadyForTest: state.primaryState === 'ready_for_test',
+ isReadyForLive: state.primaryState === 'ready_for_live',
+ isDraft: state.primaryState === 'draft',
+ isInSetup:
+ state.primaryState === 'draft' ||
+ state.primaryState === 'in_setup' ||
+ state.primaryState === 'ready_for_test' ||
+ state.primaryState === 'ready_for_live',
+ isPaused: state.primaryState === 'paused',
+ } satisfies AdminBusinessCardState;
+}
+
+export function matchesAdminBoardFilterState(state: AdminBusinessCardState, filter: AdminBoardFilter) {
+ if (filter === 'all') return true;
+ if (filter === 'archived') return state.isArchived;
+ if (state.isArchived) return false;
+
+ if (filter === 'paused') return state.isPaused;
+ if (filter === 'live') return state.isLive;
+ if (filter === 'pending_a2p') return state.shouldAppearInPendingCompliance;
+ if (filter === 'pending_setup') return state.isDraft;
+ if (filter === 'in_setup') return state.isInSetup;
+ if (filter === 'needs_attention') return state.shouldAppearInNeedsAttention;
+
return true;
}
+export function matchesAdminBoardFilter(
+ business: DashboardBusiness,
+ notificationSettings: DashboardNotificationSettings | null,
+ ownerConnected: boolean,
+ filter: AdminBoardFilter
+) {
+ const managedSummary = getManagedTwilioStatusSummary(business);
+ const nextStep = buildAdminNextStep({ business, notificationSettings, ownerConnected });
+ const onboardingConfidence = buildAdminOnboardingConfidence({
+ business,
+ notificationSettings,
+ ownerConnected,
+ successfulLeadCount: 0,
+ operatorEvents: [],
+ });
+ const state = buildAdminBusinessCardState({
+ business,
+ notificationSettings,
+ ownerConnected,
+ nextStep,
+ managedSummary,
+ onboardingConfidence,
+ lastIssue: {
+ state: 'healthy',
+ tone: 'neutral',
+ summary: 'No open issues recorded',
+ detail: 'Recent business events do not show an unresolved failure or blocker right now.',
+ createdAt: null,
+ categoryLabel: null,
+ statusLabel: null,
+ eventType: null,
+ remediationStepKey: null,
+ },
+ testSmsTruth: {
+ state: 'not_run',
+ label: 'Not run',
+ tone: 'neutral',
+ summary: 'No admin test SMS recorded',
+ detail: 'Run a test SMS from this page before treating delivery as proven.',
+ reason: null,
+ lastAttemptAt: null,
+ eventType: null,
+ },
+ missedCallValidation: {
+ state: 'not_run',
+ label: 'Not run',
+ tone: 'neutral',
+ summary: 'Missed-call flow not validated yet',
+ detail: 'Run one real missed call from start to finish, or record a manual confirmation note if you validated it outside this console.',
+ verifiedAt: null,
+ sourceLabel: null,
+ evidenceSummary: null,
+ relatedLeadId: null,
+ latestRelatedEventAt: null,
+ countsAsLaunchProof: false,
+ },
+ });
+
+ return matchesAdminBoardFilterState(state, filter);
+}
+
function compactBody(value: string, maxLength = 120) {
const trimmed = value.trim();
if (!trimmed) return 'No extra detail recorded.';
diff --git a/tests/admin-dashboard.test.ts b/tests/admin-dashboard.test.ts
index bc7d50a..29de24b 100644
--- a/tests/admin-dashboard.test.ts
+++ b/tests/admin-dashboard.test.ts
@@ -14,6 +14,7 @@ import {
} from '@prisma/client';
import {
+ buildAdminBusinessCardState,
buildAdminBusinessPickerLabel,
buildAdminOnboardingConfidence,
buildAdminBusinessEvents,
@@ -22,7 +23,9 @@ import {
getAdminTestSmsConfidenceState,
getDeleteTestBusinessBlockedReason,
matchesAdminBoardFilter,
+ matchesAdminBoardFilterState,
} from '../lib/admin-dashboard.ts';
+import { getManagedTwilioStatusSummary } from '../lib/managed-twilio-status.ts';
function createBusiness(overrides: Record = {}) {
return {
@@ -77,6 +80,96 @@ function createNotificationSettings(overrides: Record = {}) {
};
}
+function createLastIssue(overrides: Record = {}) {
+ return {
+ state: 'healthy' as const,
+ tone: 'neutral' as const,
+ summary: 'No open issues recorded',
+ detail: 'Recent business events do not show an unresolved failure or blocker right now.',
+ createdAt: null,
+ categoryLabel: null,
+ statusLabel: null,
+ eventType: null,
+ remediationStepKey: null,
+ ...overrides,
+ };
+}
+
+function createTestSmsTruth(overrides: Record = {}) {
+ return {
+ state: 'not_run' as const,
+ label: 'Not run',
+ tone: 'neutral' as const,
+ summary: 'No admin test SMS recorded',
+ detail: 'Run a test SMS from this page before treating delivery as proven.',
+ reason: null,
+ lastAttemptAt: null,
+ eventType: null,
+ ...overrides,
+ };
+}
+
+function createMissedCallValidation(overrides: Record = {}) {
+ return {
+ state: 'not_run' as const,
+ label: 'Not run',
+ tone: 'neutral' as const,
+ summary: 'Missed-call flow not validated yet',
+ detail: 'Run one real missed call from start to finish, or record a manual confirmation note if you validated it outside this console.',
+ verifiedAt: null,
+ sourceLabel: null,
+ evidenceSummary: null,
+ relatedLeadId: null,
+ latestRelatedEventAt: null,
+ countsAsLaunchProof: false,
+ ...overrides,
+ };
+}
+
+function createCardState(params: {
+ business?: Record;
+ notificationSettings?: Record;
+ ownerConnected?: boolean;
+ successfulLeadCount?: number;
+ operatorEvents?: Array<{ type: string; status: OperatorEventStatus; createdAt: Date }>;
+ missedCallValidation?: Record;
+ lastIssue?: Record;
+}) {
+ const business = createBusiness(params.business);
+ const notificationSettings = createNotificationSettings(params.notificationSettings);
+ const nextStep = buildAdminNextStep({
+ business,
+ notificationSettings,
+ ownerConnected: params.ownerConnected ?? true,
+ });
+ const onboardingConfidence = buildAdminOnboardingConfidence({
+ business,
+ notificationSettings,
+ ownerConnected: params.ownerConnected ?? true,
+ successfulLeadCount: params.successfulLeadCount ?? 0,
+ operatorEvents: params.operatorEvents || [],
+ missedCallValidation: params.missedCallValidation ? createMissedCallValidation(params.missedCallValidation) : undefined,
+ });
+
+ return buildAdminBusinessCardState({
+ business,
+ notificationSettings,
+ ownerConnected: params.ownerConnected ?? true,
+ nextStep,
+ managedSummary: getManagedTwilioStatusSummary(business),
+ onboardingConfidence,
+ lastIssue: createLastIssue(params.lastIssue),
+ testSmsTruth: createTestSmsTruth(
+ params.operatorEvents?.some((event) => event.type === 'admin.test_sms_delivered')
+ ? { state: 'delivered', label: 'Delivered', tone: 'success', summary: 'Test SMS delivered', detail: 'Delivery confirmed.' }
+ : params.operatorEvents?.some((event) => event.type === 'admin.test_sms_failed' || event.type === 'admin.test_sms_delivery_failed')
+ ? { state: 'failed', label: 'Failed', tone: 'attention', summary: 'Test SMS failed', detail: 'Delivery failed.' }
+ : {}
+ ),
+ missedCallValidation: createMissedCallValidation(params.missedCallValidation),
+ });
+}
+
test('next-step guidance calls out webhook recovery and live health clearly', () => {
const webhookMissing = buildAdminNextStep({
business: createBusiness({ twilioWebhookSyncedAt: null, managedTwilioStatus: ManagedTwilioStatus.AWAITING_BUSINESS_VERIFICATION }),
@@ -97,8 +190,9 @@ test('next-step guidance calls out webhook recovery and live health clearly', ()
ownerConnected: true,
});
- assert.equal(healthy.title, 'Business is live and healthy');
+ assert.equal(healthy.title, 'Business is live');
assert.equal(healthy.tone, 'healthy');
+ assert.equal(healthy.actionLabel, 'No action needed');
});
test('next-step guidance stays explicit for owner setup and pending A2P review', () => {
@@ -139,7 +233,7 @@ test('next-step guidance stays explicit for owner setup and pending A2P review',
});
assert.equal(invitedOwner.title, 'Owner invitation is still pending');
- assert.equal(invitedOwner.actionLabel, 'Review owner setup');
+ assert.equal(invitedOwner.actionLabel, 'Invite or connect owner');
const pendingA2p = buildAdminNextStep({
business: createBusiness({
@@ -218,6 +312,167 @@ test('board filters and delete guard stay conservative', () => {
);
});
+test('business card state turns missing live proof into live-with-warnings instead of healthy', () => {
+ const state = createCardState({
+ business: {
+ provisioningStatus: BusinessProvisioningStatus.LIVE,
+ managedTwilioStatus: ManagedTwilioStatus.COMPLIANT_LIVE,
+ a2pApprovedAt: new Date('2026-04-17T00:00:00.000Z'),
+ },
+ successfulLeadCount: 0,
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_delivered',
+ status: OperatorEventStatus.SUCCESS,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ });
+
+ assert.equal(state.primaryState, 'live_with_warnings');
+ assert.equal(state.primaryLabel, 'Live with warnings');
+ assert.equal(state.primaryReason, 'Missed-call flow has not been fully validated.');
+ assert.equal(state.nextActionLabel, 'Validate missed-call flow');
+ assert.equal(state.badges.some((badge) => badge.label === 'Missed-call proof missing'), true);
+});
+
+test('business card state keeps a clean live business decisive', () => {
+ const state = createCardState({
+ business: {
+ provisioningStatus: BusinessProvisioningStatus.LIVE,
+ managedTwilioStatus: ManagedTwilioStatus.COMPLIANT_LIVE,
+ a2pApprovedAt: new Date('2026-04-17T00:00:00.000Z'),
+ },
+ successfulLeadCount: 1,
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_delivered',
+ status: OperatorEventStatus.SUCCESS,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ missedCallValidation: {
+ countsAsLaunchProof: true,
+ detail: 'Recent event sequence proves the missed-call flow reached the owner alert path.',
+ },
+ });
+
+ assert.equal(state.primaryState, 'live');
+ assert.equal(state.nextActionLabel, 'No action needed');
+});
+
+test('archived businesses stay out of active needs-attention flow and point to restore', () => {
+ const state = createCardState({
+ business: {
+ archivedAt: new Date('2026-04-16T00:00:00.000Z'),
+ provisioningStatus: BusinessProvisioningStatus.LIVE,
+ },
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_failed',
+ status: OperatorEventStatus.FAILED,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ });
+
+ assert.equal(state.primaryState, 'archived');
+ assert.equal(state.nextActionLabel, 'Restore business');
+ assert.equal(state.shouldAppearInNeedsAttention, false);
+ assert.equal(matchesAdminBoardFilterState(state, 'needs_attention'), false);
+ assert.equal(matchesAdminBoardFilterState(state, 'archived'), true);
+});
+
+test('business card state uses specific next actions for provisioning and test SMS failures', () => {
+ const provisioningFailed = createCardState({
+ business: {
+ provisioningError: 'Messaging service attach failed.',
+ },
+ });
+ assert.equal(provisioningFailed.primaryState, 'provisioning_failed');
+ assert.equal(provisioningFailed.nextActionLabel, 'Fix provisioning');
+
+ const smsFailed = createCardState({
+ business: {
+ managedTwilioStatus: ManagedTwilioStatus.COMPLIANT_LIVE,
+ a2pApprovedAt: new Date('2026-04-17T00:00:00.000Z'),
+ },
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_failed',
+ status: OperatorEventStatus.FAILED,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ });
+ assert.equal(smsFailed.nextActionLabel, 'Send test SMS');
+});
+
+test('business card badges stay deduped and capped', () => {
+ const state = createCardState({
+ business: {
+ provisioningStatus: BusinessProvisioningStatus.LIVE,
+ managedTwilioStatus: ManagedTwilioStatus.COMPLIANT_LIVE,
+ a2pApprovedAt: new Date('2026-04-17T00:00:00.000Z'),
+ },
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_delivered',
+ status: OperatorEventStatus.SUCCESS,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ });
+
+ const labels = state.badges.map((badge) => badge.label);
+ assert.equal(new Set(labels).size, labels.length);
+ assert.equal(labels.length <= 3, true);
+});
+
+test('state-based filters use the same card model as the board counts', () => {
+ const states = [
+ createCardState({
+ business: {
+ archivedAt: new Date('2026-04-16T00:00:00.000Z'),
+ },
+ }),
+ createCardState({
+ business: {
+ provisioningError: 'Messaging service attach failed.',
+ },
+ }),
+ createCardState({
+ business: {
+ managedTwilioStatus: ManagedTwilioStatus.CAMPAIGN_SUBMITTED,
+ },
+ }),
+ createCardState({
+ business: {
+ provisioningStatus: BusinessProvisioningStatus.LIVE,
+ managedTwilioStatus: ManagedTwilioStatus.COMPLIANT_LIVE,
+ a2pApprovedAt: new Date('2026-04-17T00:00:00.000Z'),
+ },
+ successfulLeadCount: 1,
+ operatorEvents: [
+ {
+ type: 'admin.test_sms_delivered',
+ status: OperatorEventStatus.SUCCESS,
+ createdAt: new Date('2026-04-17T12:00:00.000Z'),
+ },
+ ],
+ missedCallValidation: {
+ countsAsLaunchProof: true,
+ detail: 'Recent event sequence proves the missed-call flow reached the owner alert path.',
+ },
+ }),
+ ];
+
+ assert.equal(states.filter((state) => matchesAdminBoardFilterState(state, 'needs_attention')).length, 1);
+ assert.equal(states.filter((state) => matchesAdminBoardFilterState(state, 'pending_a2p')).length, 1);
+ assert.equal(states.filter((state) => matchesAdminBoardFilterState(state, 'archived')).length, 1);
+ assert.equal(states.filter((state) => matchesAdminBoardFilterState(state, 'live')).length, 1);
+});
+
test('business picker labels keep the name primary and add a fast secondary identifier', () => {
const labelWithEmail = buildAdminBusinessPickerLabel({
business: createBusiness({
diff --git a/tests/admin-operator-routes.test.ts b/tests/admin-operator-routes.test.ts
index 51adb16..48297b4 100644
--- a/tests/admin-operator-routes.test.ts
+++ b/tests/admin-operator-routes.test.ts
@@ -18,17 +18,17 @@ test('admin routes expose support workspace and safe lifecycle controls', () =>
const appLayout = read('app/app/layout.tsx');
assert.match(adminHome, /Operator control panel/);
- assert.match(adminHome, /Fast onboard/);
+ assert.match(adminHome, /Create new business/);
assert.match(adminHome, /Create business workspace/);
- assert.match(adminHome, /Founder reset/);
+ assert.match(adminHome, /Advanced founder tools/);
assert.match(adminHome, /Delete one test\/demo business/);
assert.match(adminHome, /Advanced founder reset: delete all current businesses/);
assert.match(founderDeleteCard, /Delete this business/);
assert.match(founderDeleteCard, /Type the exact business name/);
assert.match(adminHome, /FOUNDER_DELETE_ALL_BUSINESSES_CONFIRMATION/);
assert.match(adminHome, /Business triage board/);
- assert.match(adminHome, /Go-live blocker/);
- assert.match(adminHome, /Test SMS/);
+ assert.match(adminHome, /Current reason/);
+ assert.match(adminHome, /Next action/);
assert.match(adminHome, /Open customer workspace/);
assert.match(adminHome, /Delete demo\/test business permanently/);
assert.match(adminHome, /Type business name to permanently delete/);
@@ -74,7 +74,7 @@ test('admin routes expose support workspace and safe lifecycle controls', () =>
assert.match(adminDetail, /View support workspace snapshot/);
assert.match(adminDetail, /canDeleteTestBusiness\(business\)/);
assert.match(adminDetail, /getDeleteTestBusinessBlockedReason\(business\)/);
- assert.match(adminHome, /Open blocker step/);
+ assert.match(adminHome, /Restore business/);
assert.match(activityTimeline, /Recent activity/);
assert.match(activityTimeline, /Show more activity/);
assert.match(activityTimeline, /Collapse activity/);
diff --git a/tests/managed-customer-setup.test.ts b/tests/managed-customer-setup.test.ts
index ea8cc01..62e908e 100644
--- a/tests/managed-customer-setup.test.ts
+++ b/tests/managed-customer-setup.test.ts
@@ -44,8 +44,8 @@ test('managed setup handoff is wired through customer and admin surfaces', () =>
assert.match(auth, /getOrCreateOwnedBusinessForClerkUser/);
assert.match(appLayout, /CustomerSetupWaitingPage/);
assert.match(appLayout, /getCustomerWorkspaceNotice/);
- assert.match(adminHome, /Pending setup/);
- assert.match(adminHome, /In setup/);
+ assert.match(adminHome, /Create new business/);
+ assert.match(adminHome, /Business triage board/);
assert.match(adminHome, /New public pilot signups land here waiting for founder setup/i);
assert.match(adminDetail, /This business is waiting for founder setup/);
assert.match(adminActions, /await sendCustomerReadyNotification\(business\.id\)/);