Skip to content

Settings

The settings module provides user account management (email and password changes) and organization-level configuration (telephony, CRM integrations, AI assistant rules, reporting). It uses a multi-level sidebar with URL-based deep linking and permission-gated routing.

Settings uses a hierarchical Section → Subsection model defined by two enums in settings_section_type.rs:

SectionSubsectionsPermission
AccountCredentials, NotificationsPassword account / all users
OrganizationGeneral, AI Context, AI RulesSettings:Instance:View
TeamMembers, RolesMember:Collection:List / Role:Collection:List
ChannelsPhone, Email, SocialSettings:Instance:View
IntegrationsContacts LookupSettings:Instance:View
ReportingDailySettings:Instance:View
Billing(leaf — no subsections)Owner only

Each enum variant provides slug(), label(), description(), and is_accessible(session) methods. Inaccessible sections are hidden from the sidebar.

Three route variants map to the hierarchy:

#[route("/settings")]
SettingsView {},
#[route("/settings/:section")]
SettingsSectionView { section: String },
#[route("/settings/:section/:subsection")]
SettingsSubsectionView { section: String, subsection: String },

All three delegate to SettingsShell, which resolves the URL via resolve_settings_route():

URLResolution
/settingsRedirects to the first accessible subsection (usually /settings/account/credentials)
/settings/organizationRedirects to the first accessible subsection of Organization
/settings/channels/emailRenders the Email page directly
/settings/billingRenders Billing as a leaf section (no subsection)
Invalid or inaccessible slugFalls back to the first globally accessible subsection

Redirects use nav.replace() to avoid polluting browser history. The SettingsResolution enum has four variants: Exact, RedirectToSubsection, RedirectToSection, and NoAccess.

SettingsShell (settings_shell_component.rs) is the top-level container:

ViewContainer (max-w-6xl)
├─ SettingsMobileNav (md:hidden) — Select dropdown with grouped options
└─ Flex (md:flex-row md:gap-8)
├─ SettingsSidebar (md:w-60, md:sticky top-4) — desktop only
└─ Main content (flex-1 min-w-0)
└─ Page component (dispatched by section/subsection)

SettingsSidebar (settings_sidebar_component.rs) renders section headings with subsection links. The active subsection shows a primary-colored left indicator bar and bg-accent/60 background.

On mobile (below md), the sidebar is replaced by a Select dropdown with options grouped by section.

Users change their login email through a password-verified flow.

PUT /api/settings/account/email
→ Trim and lowercase the new email
→ Validate format (must contain @ and .)
→ Verify current password against stored bcrypt hash
→ Check new email isn't already in use
→ Update email in email_password_account table

Request:

pub struct UpdateEmailData {
pub new_email: String,
pub current_password: String,
}

Possible responses: Success, IncorrectPassword, InvalidEmail, EmailAlreadyInUse.

Users change their password by providing the current one and a new one (minimum 8 characters).

PUT /api/settings/account/password
→ Validate new password ≥ 8 characters
→ Verify current password against stored bcrypt hash
→ Hash new password with bcrypt (DEFAULT_COST)
→ Update password_hash in email_password_account table

Request:

pub struct UpdatePasswordData {
pub current_password: String,
pub new_password: String,
}

Possible responses: Success, IncorrectPassword, PasswordTooShort.

GET /api/settings retrieves and PUT /api/settings updates the organization’s configuration.

pub struct SettingsData {
pub name: String,
pub twilio_account_sid: Option<String>,
pub twilio_auth_token: Option<String>,
pub client_lookup_url: Option<String>,
pub client_lookup_auth: Option<String>,
pub report_daily_enabled: bool,
pub report_per_call_enabled: bool,
pub report_email_addresses: Vec<String>,
}

Each subsection maps to a dedicated page component. See the Navigation Architecture section above for the full section → subsection hierarchy and permission requirements.

Two reusable components provide consistent structure across all settings pages.

Every settings page starts with a SettingsPageHeader (settings_page_header_component.rs):

SettingsPageHeader {
title: SettingsSubsection::OrganizationGeneral.label().to_string(),
description: SettingsSubsection::OrganizationGeneral.description().to_string(),
actions: rsx! { Button { "New Rule" } }, // optional
}

Renders a text-2xl title, an optional text-muted-foreground description, and an optional actions slot (top-right).

Groups related content within a page (settings_section_card_component.rs):

SettingsSectionCard {
title: "Identity",
description: "Who you are — displayed in your AI's greetings and across the app.",
footer: rsx! { SaveButton { status: save_status } },
IdentitySection { form }
}

Includes an optional header (title + description + action), a body (px-6 py-5), and an optional footer (border-t bg-muted/30) typically used for save buttons.

A new initials badge component (src/ui/avatar_ui.rs) with three sizes (Xs 24px, Sm 32px, Md 36px) and a bg-primary/10 text-primary color scheme. Used in the Team Members list.

Standardizes save button feedback (src/shared/utils/run_save.rs):

run_save(update_organization_profile_api(form()), save_status, toaster).await;

Cycles through SavingSaved (2 seconds) → Idle. On error, resets to Idle and shows a toast. Auto-detects 403 responses as “Access denied”.

Phone numbers are managed within the Telephony tab via the PhoneManagement component — there are no standalone /phones routes. The Telephony tab uses nested sub-tabs: Configuration (Twilio settings) and Phone Numbers (card grid).

PhoneManagement is a self-contained component at src/mods/settings/components/phone_management_component.rs. It renders:

  • Card grid — responsive grid (grid-cols-1 md:grid-cols-2 lg:grid-cols-3) of PhoneCard components
  • Inline detail panel — click a card to expand PhoneDetails below the grid for viewing/editing agent assignments
  • Buy flow — a collapsible BuyPhone section toggled by the “Buy Number” button
// Permission gates
can_create: PhonePermission::Collection(PhoneCollectionPermission::Create)
can_view: PhonePermission::Instance(PhoneInstancePermission::View)
can_update: PhonePermission::Instance(PhoneInstancePermission::Update)

The component loads phone data, voice agents, and text agents concurrently via use_resource. Errors display through the shared ResourceError component with 403-aware messaging.

The Integrations tab includes a live test to verify CRM connectivity before saving.

POST /api/settings/integrations/test-lookup
→ Send GET request to configured lookup URL with ?phone={number}
→ Include loquent_x_check header if auth token is configured
→ Return status code and response body

The UI displays a color-coded status badge (green for 2xx, red otherwise) and the raw response body.

Report email addresses use a HashSet diff approach during updates — the system compares existing addresses against the desired set, deletes removed ones, and inserts new ones within a single database transaction.

Organizations configure business identity, contact details, operating hours, and AI-specific context. The profile powers AI instruction generation in the agent module.

GET /api/settings/organization-profile

Returns current profile or defaults if none exists.

Response:

pub struct OrganizationProfileData {
// Identity
pub description: Option<String>, // "Full-service dental practice"
pub legal_name: Option<String>, // "Acme Dental LLC"
pub industry: Option<String>, // "Healthcare"
pub services: Vec<String>, // ["Cleanings", "Fillings", "Root Canals"]
pub founded_year: Option<i16>,
// Contact
pub public_phone: Option<String>,
pub public_email: Option<String>,
pub website_url: Option<String>,
pub appointment_url: Option<String>,
pub address: Option<String>,
pub service_area: Option<String>,
// Languages & Location
pub primary_language: String, // ISO 639-1 code: "en", "es", etc.
pub secondary_languages: Vec<String>,
pub timezone: Option<String>, // IANA timezone: "America/New_York"
// Operating Hours
pub hours: Vec<OrganizationHoursData>,
// AI Context
pub after_hours_message: Option<String>, // Custom message for closed hours
pub voicemail_message: Option<String>, // Voicemail handling instructions
pub emergency_contact: Option<String>,
pub additional_context: Option<String>,
pub compliance_notes: Option<String>,
// Social
pub social_links: HashMap<String, String>, // "facebook" → URL
}
pub struct OrganizationHoursData {
pub day_of_week: i16, // 0=Mon, 1=Tue, ..., 6=Sun
pub is_open: bool,
pub open_time: Option<String>, // "09:00"
pub close_time: Option<String>, // "17:00"
}
PUT /api/settings/organization-profile

Upserts the profile and replaces all operating hours in a single transaction.

Processing:

  • Existing profile → update all fields + updated_at
  • No profile → insert with created_at and updated_at
  • Hours: delete all existing rows, reinsert from payload with created_at and updated_at set to the current UTC timestamp
  • Social links stored as JSON

The build_organization_context service formats profile data into markdown for injection into agent system prompts.

Output structure:

## Organization Context
- Description: {description}
- Legal Name: {legal_name}
- Industry: {industry}
- Services: {services}
- Founded: {founded_year}
- Phone: {public_phone}
- Email: {public_email}
- Website: {website_url}
- Appointments: {appointment_url}
- Address: {address}
- Service Area: {service_area}
- Primary Language: {primary_language}
- Secondary Languages: {secondary_languages}
- Timezone: {timezone}
### Operating Hours
- Monday: 09:00 – 17:00
- Tuesday: 09:00 – 17:00
- Wednesday: Closed
...
- After Hours Message: {after_hours_message}
- Voicemail Message: {voicemail_message}
- Emergency Contact: {emergency_contact}
- Additional Context: {additional_context}
- Compliance Notes: {compliance_notes}

Empty fields are omitted. If no profile exists, returns an empty string.

Admins define organization-level rules that shape AI behavior across 5 scoped areas. Each rule targets a specific area and is injected into that area’s prompt as a priority directive.

AreaEnum ValueWhere Injected
AssistantassistantChat assistant system prompt
Voice Agentvoice_agentOpenAI and Gemini realtime agent instructions
Task Suggestionstask_suggestionsText agent reply suggestion context
Memory Updatememory_updateContact memory synthesis prompt
Task Creatortask_creatorTask extraction from calls and messages

Each area uses the same injection pattern — get_active_rules_for_area(org_id, area) fetches active rules, then format_rules_for_prompt() formats them into the prompt section.

Table: organization_ai_rule

ColumnTypeDescription
idUUIDPrimary key
organization_idUUIDFK → organization (CASCADE delete)
titletextRule display name
contenttextFull rule text
areatextTarget area (default: 'assistant')
is_activebooleanActive/inactive toggle (default: true)
sort_orderintegerDisplay ordering (default: 0)
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

Indexed on (organization_id, area) via idx_organization_ai_rule_org_area.

// Request — used for create and update
pub struct AiRuleData {
pub title: String,
pub content: String,
pub is_active: bool,
}
// Response
pub struct AiRule {
pub id: String,
pub title: String,
pub content: String,
pub is_active: bool,
pub sort_order: i32,
}
RouteMethodPermissionDescription
/api/settings/ai-rulesGETSettings:Instance:ViewList all rules for the org, ordered by sort_order then created_at
/api/settings/ai-rulesPOSTSettings:Instance:UpdateCreate a new rule
/api/settings/ai-rules/{id}PUTSettings:Instance:UpdateUpdate a rule (validates org ownership)
/api/settings/ai-rules/{id}DELETESettings:Instance:UpdateDelete a rule (validates org ownership)

All endpoints validate that the rule belongs to the requesting user’s organization. Update and delete return 404 if the rule doesn’t exist or belongs to a different org.

Each area fetches and formats its rules using a shared service:

let rules = get_active_rules_for_area(org_id, AiRuleArea::Assistant).await?;
let section = format_rules_for_prompt(&rules, "ORGANIZATION RULES");

This produces a markdown section injected into the prompt:

## ORGANIZATION RULES
The following rules are defined by the organization administrator.
You MUST follow these rules in ALL interactions.
These rules take precedence over your default behavior.
**RULE: DO NOT CONTACT SIGNED CONTRACTS**
When sending a promotional campaign, check the custom field "BB-NT".
If it's "Contract Signed", don't send the message.

The heading varies by area (e.g., “ORGANIZATION RULES”, “MEMORY UPDATE RULES”). The GET /api/settings/ai-rules endpoint accepts an optional ?area= query parameter to filter rules by area.

The Assistant Rules tab (icon: Sparkles) renders two components:

  • AiRuleList — card grid with Active/Inactive badges, inline edit, create/delete controls. Shows “No AI rules created yet.” when empty.
  • AiRuleForm — reusable create/edit form with title input, content textarea, and active checkbox. Submit is disabled until both title and content are filled.

Create, edit, and delete buttons are gated behind Settings:Instance:Update. View-only users see the rule list without mutation controls.

RouteMethodDescription
GET /api/settings/accountGETRetrieve current user’s email
PUT /api/settings/account/emailPUTChange login email
PUT /api/settings/account/passwordPUTChange password
GET /api/settingsGETRetrieve organization settings
PUT /api/settingsPUTUpdate organization settings
GET /api/settings/organization-profileGETRetrieve organization profile
PUT /api/settings/organization-profilePUTUpdate organization profile
POST /api/settings/integrations/test-lookupPOSTTest CRM lookup integration
GET /api/settings/ai-rulesGETList organization AI assistant rules
POST /api/settings/ai-rulesPOSTCreate AI assistant rule
PUT /api/settings/ai-rules/{id}PUTUpdate AI assistant rule
DELETE /api/settings/ai-rules/{id}DELETEDelete AI assistant rule

All endpoints require an authenticated session.

The Settings page adapts to screen size:

BreakpointBehavior
Below md (mobile)Desktop sidebar hidden, replaced by a grouped Select dropdown for navigation. Forms stack single-column.
md and above (desktop)Sticky sidebar with section headings and subsection links. Content area fills remaining width.

The mobile Select groups subsections under their parent section labels. Leaf sections like Billing appear as standalone items. Selecting an option navigates via the router.

src/mods/settings/
├── api/
│ ├── get_ai_rules_api.rs # List rules
│ ├── create_ai_rule_api.rs # Create rule
│ ├── update_ai_rule_api.rs # Update rule
│ ├── delete_ai_rule_api.rs # Delete rule
│ ├── get_organization_profile_api.rs
│ ├── update_organization_profile_api.rs
│ └── ... # Account, settings, test-lookup
├── types/
│ ├── settings_section_type.rs # SettingsSection + SettingsSubsection enums
│ ├── settings_section_resolver.rs # resolve_settings_route() + SettingsResolution
│ ├── ai_rule_data_type.rs # AiRuleData, AiRule
│ ├── organization_profile_data_type.rs
│ └── ... # Account, settings types
├── components/
│ ├── settings_shell_component.rs # Shell + mobile nav + route resolution
│ ├── settings_sidebar_component.rs # Desktop sidebar navigation
│ ├── settings_page_header_component.rs # Standardized page header
│ ├── settings_section_card_component.rs # Grouped content card
│ ├── pages/ # 13 subsection page components
│ │ ├── account_credentials_page.rs
│ │ ├── account_notifications_page.rs
│ │ ├── organization_general_page.rs
│ │ ├── organization_ai_context_page.rs
│ │ ├── organization_ai_rules_page.rs
│ │ ├── team_members_page.rs
│ │ ├── team_roles_page.rs
│ │ ├── channels_phone_page.rs
│ │ ├── channels_email_page.rs
│ │ ├── channels_social_page.rs
│ │ ├── integrations_contacts_lookup_page.rs
│ │ ├── reporting_daily_page.rs
│ │ └── billing_page.rs
│ ├── ai_rule_list_component.rs # Rule card grid with CRUD
│ ├── ai_rule_form_component.rs # Create/edit form
│ ├── organization_general_form_component.rs # Identity, Contact, Languages, Social, Hours
│ ├── organization_ai_context_form_component.rs # AI Context, Voicemail, Compliance
│ ├── phone_management_component.rs # Phone card grid, detail panel, buy flow
│ └── ... # MemberList, EmailList, etc.
└── views/
└── settings_view.rs # SettingsView, SettingsSectionView, SettingsSubsectionView
  • Agent — organization profile powers AI instruction generation
  • Contact — CRM lookup integration resolves caller identity
  • Phone — phone number data types and APIs used by PhoneManagement
  • Report — daily and per-call reports configured here
  • Twilio — telephony credentials managed in the Telephony tab