04-clinicalwave: W2filled11 citations

Clinical Activities — Activity Timeline and Audit Trail

Audiences: doctor, nurse, developer, internal

Clinical Activities — Activity Timeline and Audit Trail

The Activity module automatically tracks which clinical resources each user has viewed or interacted with, creating a per-user recent-activity timeline that powers "recently viewed patients", dashboard quick-access, and operational audit trails.

Business Purpose

In a busy clinic, doctors and nurses navigate dozens of patient charts daily. Without a "recently viewed" feed, they must search from scratch each time. Additionally, compliance and security teams need to know who accessed which patient records and when.

Dudoxx HMS's Activity module provides:

  1. Auto-tracked resource viewsActivityInterceptor automatically records viewed_patient events when any clinical user accesses GET /patients/:id. No manual tracking needed in clinical controllers.
  2. Manual activity trackingActivityService.trackActivity() can be called explicitly from any service to record arbitrary activityType / resourceType / resourceId events.
  3. Recent activity feedGET /api/v1/activity/recent?limit=50 returns a per-user timeline of recent resource interactions, ordered by timestamp descending.
  4. Cron cleanup — a scheduled job (@Cron) in ActivityService periodically prunes old RecentActivity rows to prevent unbounded growth.

This creates an operational activity log without requiring developers to instrument every endpoint manually.

Audiences

  • Investor: Activity tracking demonstrates the platform's operational maturity — clinics can demonstrate GDPR-compliant access logging per patient record.
  • Clinical buyer (doctor/nurse/receptionist): Doctors see a "recently viewed" list in their portal home screen for quick navigation. The system is invisible during normal use — it works automatically.
  • Developer/partner: GET /api/v1/activity/recent — returns RecentActivity[]. POST /api/v1/activity/track — manual event recording. The interceptor fires automatically on GET /patients/:id requests.
  • Internal (ops/support): Data in Prisma ddx_api_main (RecentActivity model). Non-UUID user IDs (service accounts) are skipped with a debug log (activity.service.ts:23). Cron cleanup runs on schedule via @nestjs/schedule.

Architecture

diagram
ActivityModule
  ├── ActivityController   — GET /activity/recent + GET /activity/by-resource
  ├── ActivityService      — Prisma CRUD + cron cleanup
  └── ActivityInterceptor  — auto-tracks GET /patients/:id → viewed_patient

ActivityInterceptor is a NestInterceptor that hooks into the response pipeline. It reads the route path after response completion (tap() operator in RxJS) — so it never blocks the response. It uses setImmediate() to defer the database write to the next event loop tick, ensuring zero latency impact on the clinical request.

The ActivityInterceptor is registered in ActivityModule and applied globally or to specific routes. Only GET requests to paths matching /patients/{uuid} generate automatic viewed_patient events — other resource types require explicit trackActivity() calls.

Tech Stack & Choices

LayerTechnologyNotes
StoragePrisma ddx_api_main (RecentActivity)Per-user rows with activityType, resourceType, resourceId, resourceName, metadata, timestamp
Auto-trackingActivityInterceptor (NestJS NestInterceptor)RxJS tap() + setImmediate() for non-blocking write
Cleanup@nestjs/schedule @Cron decoratorPeriodic pruning of old rows
AuthUser ID from request.user.id (gateway headers)Non-UUID IDs (service accounts) are silently skipped
Error handlingtry/catch with logger.errorActivity failures never propagate to the caller

Key design: Activity errors are swallowed (catch(() => {})) — this is intentional. A failed activity write must never affect the actual clinical operation. The tradeoff is that activity data can have gaps if the DB is briefly unavailable.

Data Flow

Doctor views patient chart (auto-tracked)

Business outcome: Doctor opens patient "Maria Schmidt"'s chart; 10 seconds later, "Maria Schmidt" appears at the top of the doctor's "Recently Viewed" list on the dashboard.

Technical mechanism:

  1. Doctor's browser sends GET /api/v1/patients/{uuid}.
  2. PatientsController returns the patient data normally.
  3. ActivityInterceptor.intercept() (activity.interceptor.ts:16) fires in the tap() post-response hook.
  4. Checks: method === 'GET' AND path matches /patients/{id} regex AND id !== 'favorites'.
  5. Calls setImmediate(() => activityService.trackActivity(userId, { activityType: 'viewed_patient', resourceType: 'Patient', resourceId: match[1] }).catch(() => {})).
  6. ActivityService.trackActivity() (activity.service.ts:22) validates UUID format (skips service accounts), writes RecentActivity Prisma row.

Doctor fetches recent activity feed

GET /api/v1/activity/recent?limit=20ActivityControllerActivityService.getRecentActivity(userId, limit)prisma.recentActivity.findMany({ where: { userId }, orderBy: { timestamp: 'desc' }, take: limit }).

Manual activity tracking (from other services)

Any service can call activityService.trackActivity(userId, { activityType: 'created_prescription', resourceType: 'MedicationRequest', resourceId: prescriptionId }) to record custom events in the activity timeline.

Implicated Code

  • ddx-api/src/misc/activity/activity.interceptor.ts:12ActivityInterceptorNestInterceptor; hooks GET /patients/:id responses
  • ddx-api/src/misc/activity/activity.interceptor.ts:26tap() — post-response hook; reads route path, matches patient UUID pattern
  • ddx-api/src/misc/activity/activity.interceptor.ts:31setImmediate() — defers DB write to next event loop tick (non-blocking)
  • ddx-api/src/misc/activity/activity.service.ts:8ActivityService — Prisma-based activity CRUD + cron cleanup
  • ddx-api/src/misc/activity/activity.service.ts:14getRecentActivity() — paginated per-user timeline, ordered by timestamp desc
  • ddx-api/src/misc/activity/activity.service.ts:22trackActivity() — UUID guard + Prisma write + error swallowing
  • ddx-api/src/misc/activity/activity.service.ts:23 — UUID guard — skips non-UUID user IDs (service accounts would violate FK constraint)
  • ddx-api/src/misc/activity/activity.service.ts:52getActivityByResource() — per-resource access log (who accessed this patient record and when)

Operational Notes

  • Only patients auto-tracked: The ActivityInterceptor currently only auto-tracks GET /patients/:id requests. Other resource types (visits, prescriptions, documents) require explicit trackActivity() calls from the relevant service.
  • Service accounts skipped: ActivityService.trackActivity() skips non-UUID user IDs with a debug log. Service accounts (seeders, cron jobs, TUCAN agent) do not pollute the activity timeline.
  • Error swallowing is intentional: Activity write failures are caught and logged but never re-thrown. A broken RecentActivity write never breaks clinical workflows.
  • Cron pruning: The @Cron decorator in ActivityService runs a periodic cleanup of old rows. Check the cron expression to ensure pruning does not run during peak clinical hours (typically 7am–7pm).
  • FK constraint on userId: RecentActivity.userId is a foreign key to User. If a user is deleted, their activity rows must be pruned first or the delete will fail with a FK violation.
  • No SSE: Activity events are NOT streamed via SSE — they are pull-based (GET /activity/recent). If a "live feed" of clinical activity is needed, it would require an SSE integration on top of this service.
  • Clinical Visits — Visit access is a primary activity type
  • Clinical Forms — Form submissions can be tracked as activities
  • Assessments — Assessment completions may generate activity events
  • Intake Engine — Intake events generate SSE (not activity) — distinct systems
  • Medical Cards — Medical card views may auto-track patient resource access
    Clinical Activities — Activity Timeline and Audit Trail — Dudoxx Docs | Dudoxx