Skip to content

Report

The report module generates AI-powered daily business summaries. Organizations configure which data sources to include (calls, messages, tasks, contacts), customize the AI prompt, test it in real time, and receive scheduled reports delivered as in-app notifications with email.

Hourly cron job runs:
→ For each org with reports enabled:
→ Check if local hour matches send_at_hour
→ Gather yesterday's data (calls, messages, tasks, contacts)
→ Build structured prompt with real data
→ Send to LLM via OpenRouter → get markdown summary
→ Persist as report_instance
→ Notify recipients (in-app + email)

Stores per-organization report configuration.

ColumnTypeDescription
idUUIDPrimary key
organization_idUUIDOwning organization
enabledboolWhether daily reports are active
send_at_houri16Hour of day (0–23) in org’s local timezone
promptOption<String>Custom AI prompt (falls back to default)
include_callsboolInclude call data
include_messagesboolInclude message data
include_tasksboolInclude task data
include_contactsboolInclude contact data

Maps reports to users who receive them.

ColumnTypeDescription
idUUIDPrimary key
report_idUUIDFK → report.id
user_idUUIDFK → user receiving the report

A B-tree index on report_id speeds up recipient lookups.

Persists each generated report for viewing in the UI.

ColumnTypeDescription
idUUIDPrimary key
report_idUUIDFK → report.id
organization_idUUIDOwning organization
titleStringe.g. “Daily Report - Acme Corp”
bodyStringMarkdown content from AI
period_startDateTimeUtcStart of reporting window
period_endDateTimeUtcEnd of reporting window
data_sourcesJSONReportDataSources snapshot with entity counts
created_atDateTimeUtcWhen generated

Snapshot of which entities were included and their counts:

pub struct ReportDataSources {
pub calls: Option<u32>,
pub messages: Option<u32>,
pub tasks: Option<u32>,
pub contacts: Option<u32>,
}

Full report for detail views:

pub struct ReportInstance {
pub id: String,
pub title: String,
pub body: String, // Markdown content
pub period_start: String,
pub period_end: String,
pub data_sources: ReportDataSources,
pub created_at: String,
}

List view variant (no body field) used in ReportInstanceListResponse.

The generate_daily_report service (generate_daily_report_service.rs) handles multi-entity data gathering and LLM generation.

Process:

  1. Compute yesterday’s time window in the org’s local timezone, convert to UTC
  2. For each enabled data source, query the relevant tables:
    • Calls — filters to substantive calls only (has transcription OR duration ≥ 30s), truncates transcriptions to 500 chars
    • Messages — groups by contact with inbound/outbound counts per contact
    • Tasks — completed or created tasks in the window
    • Contacts — new or updated contacts
  3. Build a structured markdown context with all gathered data
  4. Send to LLM via OpenRouter using the GenerateReport AI area (model: gemini-3.1-pro-preview)
  5. Return the markdown body and data source counts, or None if no substantive activity

Key type:

pub struct ReportGenerationConfig {
pub organization_id: Uuid,
pub custom_prompt: Option<String>,
pub tz: chrono_tz::Tz,
pub include_calls: bool,
pub include_messages: bool,
pub include_tasks: bool,
pub include_contacts: bool,
}

Default prompt (when org has no custom prompt):

Summarize yesterday’s business activity. Highlight key themes, important conversations, action items, and any contacts that need follow-up. Cross-reference insights across calls, messages, tasks, and contacts where relevant.

The send_timezone_aware_daily_reports service runs on an hourly cron job.

Flow:

  1. Fetch all enabled reports
  2. Batch-load org timezones, recipients, and org names (avoids N+1 queries)
  3. For each report, convert current UTC time to the org’s local timezone
  4. If local_now.hour() == report.send_at_hour, generate and dispatch
  5. Persist the result as a report_instance
  6. Send notifications to all configured recipients

Users can test their prompt configuration without saving, using yesterday’s real data.

RouteMethodDescription
POST /api/reports/testPOSTFire-and-forget test generation

Input: TestReportPromptInput

pub struct TestReportPromptInput {
pub prompt: Option<String>,
pub include_calls: bool,
pub include_messages: bool,
pub include_tasks: bool,
pub include_contacts: bool,
}

The endpoint spawns a tokio::spawn background task and returns immediately. Results arrive via WebSocket.

EventPayloadDescription
report.test.completedReportTestCompletedEventReport generated successfully
report.test.failedReportTestFailedEventGeneration error
report.test.no_datanullNo activity data for yesterday

use_realtime_report_test() subscribes to WebSocket events and exposes a Signal<ReportTestState>:

pub enum ReportTestState {
Idle,
Generating,
Completed(ReportTestCompletedEvent),
Failed(String),
NoData,
}

ReportTestPanel renders inline below the prompt textarea with five states:

  • Idle — hidden
  • Generating — structured skeleton loader with theme-aware colors
  • Completed — markdown preview with period date and data source badges
  • Failed — error message with retry button
  • NoData — empty state message

The “Test prompt” button sends the current unsaved form state. After the first test, it changes to “Test again”.

RouteMethodDescription
GET /api/reports?page&page_sizeGETPaginated list of report instances
GET /api/reports/{id}GETSingle report instance detail
POST /api/reports/testPOSTTrigger test report generation

List endpoint defaults to page 0, page size 25 (max 100). Results are org-scoped and sorted by created_at descending.

The Reporting tab in Settings includes:

  • Enable toggle — turns daily reports on/off
  • Send time picker — hour-of-day select with 12h labels
  • Data source checkboxes — calls, messages, tasks, contacts grid
  • Custom prompt textarea — override the default AI prompt
  • Test prompt button — generates a preview using yesterday’s data
  • Recipient picker — select org members (validated against org membership)
  • Report list (/reports) — ReportCard components showing title, date, data source badges
  • Report detail (/reports/{id}) — full markdown body with metadata

Both views are accessible via the Reports sidebar navigation entry.

Reports use NotificationCategory::Report and NotificationEntity::Report(instance_id). Clicking a report notification navigates to the persisted report instance.

Email delivery uses a dedicated report_notification_email_template with branded styling.

src/mods/report/
├── api/
│ ├── get_report_instances_api.rs # Paginated list
│ ├── get_report_instance_api.rs # Single detail
│ └── test_report_prompt_api.rs # Fire-and-forget test
├── components/
│ ├── report_card_component.rs # List card
│ └── report_test_panel_component.rs # Inline test results
├── hooks/
│ └── use_realtime_report_test.rs # WebSocket event listener
├── services/
│ ├── generate_daily_report_service.rs # Multi-entity AI generation
│ └── send_timezone_aware_daily_reports_service.rs # Hourly cron dispatch
├── types/
│ ├── report_data_sources_type.rs # Entity count snapshot
│ ├── report_event_type.rs # WebSocket event constants
│ ├── report_instance_type.rs # Instance + summary + list types
│ └── report_test_type.rs # Test input, events, state machine
└── views/
├── report_list_view.rs # /reports
└── report_details_view.rs # /reports/{id}