Superadmin Tenant Activity Intelligence
Date: 2026-05-20 Goal: Replace platform-level-only analytics with per-tenant × per-user × per-module daily activity intelligence, with on-demand Ruby AI briefs.Architecture
Schema (Drizzle / Neon)
tenant_daily_activity (rollup, partitioned by month long-term)
| col | type | note |
|---|---|---|
| clinic_id | uuid | FK clinics |
| user_id | uuid | nullable for system events |
| module_key | text | maps from audit_logs.entity_type |
| activity_date | date | day in PKT |
| total_actions | int | sum |
| action_counts | jsonb | {create: 5, update: 2, delete: 1, view: 12, ...} |
| phi_access_count | int | from audit_logs.accessed_phi non-null |
| PK | (clinic_id, user_id, module_key, activity_date) |
(clinic_id, activity_date DESC), (activity_date DESC).
tenant_daily_briefs (Ruby outputs)
| col | type | note |
|---|---|---|
| clinic_id | uuid | |
| brief_date | date | |
| headline | text | one-liner (Busy / Quiet / ⚠ Anomalies) |
| bullets | jsonb | array of bullet strings |
| anomalies | jsonb | array, can be empty |
| model_used | text | e.g. deepseek-chat |
| trace_id | text | Langfuse trace |
| generated_at | timestamptz | |
| PK | (clinic_id, brief_date) |
Module Mapping
entity_type → module_key:
appointments→appointmentspatients,patient-files→patients/patient_filesinvoices,expenses,quotations→financeclinical-notes→clinical_notestreatment-plans→treatment_plansprescriptions→prescriptionslab→lab_trackingchat,messages→communicationdicom,ai→dicom_imaging/ai_insights- … (fallback
_other)
server/src/lib/activity-modules.ts.
Cron
server/src/cron/rollup-tenant-activity.ts- Runs 02:00 PKT (21:00 UTC) via existing cron handler
- For
yesterday(PKT):INSERT ON CONFLICT DO UPDATEaggregation fromaudit_logs - Backfill script for first 30 days on deploy:
server/scripts/backfill-tenant-activity.ts
Egress Optimization (Neon)
- Tenant list query: read only rollup columns + clinic name (no joins, single indexed range scan)
- Today’s live slice:
WHERE clinic_id = ? AND created_at >= today_start_pkt(uses existingaudit_logsindex oncreated_at) - No
SELECT *anywhere; explicit column lists - Per-user / per-module queries limited to single selected clinic_id (never full-table scans)
Ruby Brief (Langfuse-managed)
- Prompt key:
superadmin_tenant_daily_brief - Pushed via existing
server/scripts/push-langfuse-prompts.ts - Output shape:
{ headline: string, bullets: string[], anomalies: string[] }(JSON mode) - Variables:
clinic_name, date, total_actions, top_users[], modules[], phi_access_count, deletes, off_hours_actions, new_ips[] - Agent:
server/src/lib/ai/agents/tenant-brief.ts - Cached in
tenant_daily_briefs; regenerate button in UI forces refetch
UI (full master-detail replace)
ui/src/components/superadmin/PlatformAnalytics.tsxbecomes the tab router shell- New components:
TenantIntelligence.tsx— outer layoutTenantList.tsx— virtualized list with search + sort (activity / phi / name) + filter (status, plan, has-anomaly)TenantDetail.tsx— right paneRubyBriefCard.tsx— headline + bullets + anomalies + “Regenerate” buttonUserActivityTable.tsxModuleActivityBars.tsxRawFeedDrawer.tsx
- Platform overview = first virtual row “All Tenants” (pseudo-tenant)
- Default range: 7 days; date picker for any prior day
Permissions
- All
/platform/*endpoints alreadyrequireSuperadmin - Brief generation logs to
audit_logs(action: superadmin_view_tenant_brief) - PHI access counts shown but no raw PHI in any response or Ruby prompt
Out of Scope (V1)
- Real-time websocket updates
- Cross-tenant comparison charts
- Export (CSV / PDF)
- Alerting on anomalies (separate work)

