Skip to Content
Serversservers/reporting — @leadmetrics/server-reporting

servers/reporting — @leadmetrics/server-reporting

A dedicated Node.js background service that sends three scheduled daily email reports via the notification queue. It runs as a standalone process using node-cron and never dispatches emails directly — it enqueues jobs into notifications__email for the notifications server to process.

Source: apps/servers/reporting/


Why a Separate Service

ConcernReason
IsolationCron failures never affect the API or agent workers
Independent scalingReporting can run as a single-replica low-resource process
Timezone-aware schedulingTenant reports fire at 10 PM in each tenant’s local timezone — a single 30-min cron sweeps all timezones
DeduplicationEach report has a unique dedupeKey per date — re-runs on the same day are idempotent

Three Daily Reports

ReportScheduleRecipientDescription
Tenant Daily Report10 PM tenant local timeTenant (via NotificationPreference → fallback owner)Activities done, due, and overdue; period summary
Admin Tenant Report10 PM IST (16:30 UTC)ADMIN_REPORT_EMAILNew tenants, status changes, running totals by status
System Usage Report10 PM IST (16:30 UTC)ADMIN_REPORT_EMAILSessions, agent runs, tokens, cost, adapter breakdown

All three use templateSlugemail_template DB lookup → Handlebars rendering via the notifications server.


Email Templates (seeded in packages/db/src/seed.ts)

SlugSubjectKey Variables
tenant_daily_reportYour Leadmetrics daily update — {{reportDate}}tenantName, reportDate, periodSummary, activitiesDoneTable, activitiesDueTable, overdueTable, doneCount, dueCount, overdueCount
admin_tenant_reportLeadmetrics admin: tenant report — {{reportDate}}reportDate, statsRow, newTenantsTable, onboardedTable, inactiveTable, newCount, onboardedCount, inactiveCount, grandTotal
system_usage_reportLeadmetrics system: usage report — {{reportDate}}reportDate, statsRow, adapterBreakdownTable, totalRuns, totalCostUsd, totalInputTokens, totalOutputTokens, dailyActiveSessions, activeTenantCount

HTML tables are pre-rendered as strings in the job before being passed as variables. Templates use triple-brace {{{tableVar}}} Handlebars syntax for unescaped HTML injection.


Scheduler (src/scheduler.ts)

Two crons run in the same process:

Tenant cron — every 30 minutes

0,30 * * * *

Iterates all active tenants, checks getLocalHour(tenant.timezone) === 22, and enqueues a tenant daily report for any tenant whose local hour just hit 22. Deduplication via dedupeKey = tenant_daily_report__{tenantId}__{localDate} prevents double-sends.

Admin + System cron — fixed UTC

30 16 * * * # 16:30 UTC = 22:00 IST

Runs the admin tenant report and system usage report once daily.


Timezone Handling (src/timezone.ts)

resolveTenantTimezone(tenant) 1. Use tenant.timezone if set (IANA string, e.g. "Asia/Kolkata") 2. Map tenant.country to a timezone (40+ country entries) 3. Default: "Asia/Kolkata" (IST)

All UTC-conversion functions use Intl.DateTimeFormat — no external timezone library needed.


Recipients

Tenant Daily Report

  1. NotificationPreference where tenantId = X AND category = "daily_activity" — uses the stored recipients JSON array.
  2. Fallback: first TenantMember by createdAt asc (the owner) — uses user.email.
  3. If still no recipients: skipped with a warning log.

Admin & System Reports

  • Always sent to config.ADMIN_REPORT_EMAIL / config.ADMIN_REPORT_NAME.
  • Uses tenantId: "__platform__" — resolves to platform SendGrid default (no tenant provider).

Deduplication

BullMQ jobId + deduplication.id are both set to the dedupeKey. This means:

  • Re-running the trigger on the same calendar day has no effect — the jobs already exist.
  • To force a re-send (e.g. in development), clear the failed/completed jobs from Redis first:
# List and delete failed jobs docker exec ragmanager-redis redis-cli ZRANGE bull:notifications__email:failed 0 -1 docker exec ragmanager-redis redis-cli DEL bull:notifications__email:<jobId> docker exec ragmanager-redis redis-cli DEL bull:notifications__email:failed docker exec ragmanager-redis redis-cli DEL bull:notifications__email:completed

One-Off Trigger (trigger.ts)

Run all three jobs immediately (bypasses the cron schedule):

cd apps/servers/reporting tsx --env-file .env trigger.ts

Also requires the notifications server to be running:

cd apps/servers/notifications tsx --env-file .env src/index.ts

Schema Additions

Two fields were added to support system usage reporting:

model Tenant { timezone String? // IANA timezone, e.g. "Asia/Kolkata"; falls back to country map → IST } model AgentRun { inputTokens Int? // LLM input tokens for this run outputTokens Int? // LLM output tokens for this run }

Config (src/config.ts)

VariableRequiredDefaultDescription
DATABASE_URLyesPrisma connection string
REDIS_URLnoredis://localhost:6379BullMQ connection
DASHBOARD_URLnohttp://localhost:3000Used in email links
ADMIN_REPORT_EMAILnomoble@leadmetrics.aiRecipient for admin + system reports
ADMIN_REPORT_NAMEnoMoble JosephDisplay name for admin recipient
TENANT_REPORT_CRONno0,30 * * * *Cron expression for tenant sweep
ADMIN_REPORT_CRONno30 16 * * *Cron expression for admin + system reports

File Structure

apps/servers/reporting/ |-- src/ | |-- index.ts Entry point — loads config, starts scheduler, handles shutdown | |-- config.ts Zod env validation | |-- scheduler.ts Two node-cron jobs wired to job functions | |-- timezone.ts resolveTenantTimezone, getTodayBoundsUtc, getLocalHour, etc. | | | +-- jobs/ | |-- tenant-daily-report.ts Queries activities; enqueues per tenant | |-- admin-tenant-report.ts Queries tenant status changes; enqueues to admin | +-- system-usage-report.ts Queries AgentRun aggregates; enqueues to admin | |-- trigger.ts One-off script to fire all 3 jobs immediately |-- .env |-- package.json +-- tsconfig.json

© 2026 Leadmetrics — Internal use only