Clinical Activities — Activity Timeline and Audit Trail
Audiences: doctor, nurse, developer, internal
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:
- Auto-tracked resource views —
ActivityInterceptorautomatically recordsviewed_patientevents when any clinical user accessesGET /patients/:id. No manual tracking needed in clinical controllers. - Manual activity tracking —
ActivityService.trackActivity()can be called explicitly from any service to record arbitraryactivityType/resourceType/resourceIdevents. - Recent activity feed —
GET /api/v1/activity/recent?limit=50returns a per-user timeline of recent resource interactions, ordered by timestamp descending. - Cron cleanup — a scheduled job (
@Cron) inActivityServiceperiodically prunes oldRecentActivityrows 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— returnsRecentActivity[].POST /api/v1/activity/track— manual event recording. The interceptor fires automatically onGET /patients/:idrequests. - Internal (ops/support): Data in Prisma
ddx_api_main(RecentActivitymodel). Non-UUID user IDs (service accounts) are skipped with adebuglog (activity.service.ts:23). Cron cleanup runs on schedule via@nestjs/schedule.
Architecture
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
| Layer | Technology | Notes |
|---|---|---|
| Storage | Prisma ddx_api_main (RecentActivity) | Per-user rows with activityType, resourceType, resourceId, resourceName, metadata, timestamp |
| Auto-tracking | ActivityInterceptor (NestJS NestInterceptor) | RxJS tap() + setImmediate() for non-blocking write |
| Cleanup | @nestjs/schedule @Cron decorator | Periodic pruning of old rows |
| Auth | User ID from request.user.id (gateway headers) | Non-UUID IDs (service accounts) are silently skipped |
| Error handling | try/catch with logger.error | Activity 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:
- Doctor's browser sends
GET /api/v1/patients/{uuid}. PatientsControllerreturns the patient data normally.ActivityInterceptor.intercept()(activity.interceptor.ts:16) fires in thetap()post-response hook.- Checks:
method === 'GET'AND path matches/patients/{id}regex ANDid !== 'favorites'. - Calls
setImmediate(() => activityService.trackActivity(userId, { activityType: 'viewed_patient', resourceType: 'Patient', resourceId: match[1] }).catch(() => {})). ActivityService.trackActivity()(activity.service.ts:22) validates UUID format (skips service accounts), writesRecentActivityPrisma row.
Doctor fetches recent activity feed
GET /api/v1/activity/recent?limit=20 → ActivityController → ActivityService.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:12—ActivityInterceptor—NestInterceptor; hooks GET /patients/:id responsesddx-api/src/misc/activity/activity.interceptor.ts:26—tap()— post-response hook; reads route path, matches patient UUID patternddx-api/src/misc/activity/activity.interceptor.ts:31—setImmediate()— defers DB write to next event loop tick (non-blocking)ddx-api/src/misc/activity/activity.service.ts:8—ActivityService— Prisma-based activity CRUD + cron cleanupddx-api/src/misc/activity/activity.service.ts:14—getRecentActivity()— paginated per-user timeline, ordered by timestamp descddx-api/src/misc/activity/activity.service.ts:22—trackActivity()— UUID guard + Prisma write + error swallowingddx-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:52—getActivityByResource()— per-resource access log (who accessed this patient record and when)
Operational Notes
- Only patients auto-tracked: The
ActivityInterceptorcurrently only auto-tracksGET /patients/:idrequests. Other resource types (visits, prescriptions, documents) require explicittrackActivity()calls from the relevant service. - Service accounts skipped:
ActivityService.trackActivity()skips non-UUID user IDs with adebuglog. 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
RecentActivitywrite never breaks clinical workflows. - Cron pruning: The
@Crondecorator inActivityServiceruns 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.userIdis a foreign key toUser. 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.
Related Topics
- 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