Notes, Tasks and Activity Feed
Audiences: doctor, developer
Clinic staff capture unstructured thoughts (rich text, voice recordings, file attachments), coordinate work via task groups with STT-powered notes, and review a personal activity timeline that auto-tracks resource views — all wired to SSE for live UI updates.
Business Purpose
Clinical workflows generate a constant stream of context that doesn't belong in structured FHIR records but must not be lost: a dictated voice memo after a consultation, a follow-up checklist for a nurse, a scanned referral letter flagged for review. The Notes + Tasks + Activity triad covers this gap:
- Notes: personal, per-user scratchpad with folders, tags, rich-text or plain content, linked audio recordings (with async STT transcription), file attachments (with OCR extraction), and polymorphic links to any clinical entity (patient, visit, appointment, etc.).
- Tasks: structured work items grouped into
TaskGroupcontainers. Staff assign tasks to colleagues, set priorities and due dates, attach notes with voice recordings, and link tasks to FHIR entities. Categories map to clinical workflows (LAB_REVIEW, APPROVAL_REQUIRED, FOLLOW_UP, etc.). - Activity Feed: a rolling, user-scoped timeline of resource views (currently: patient profile accesses), auto-populated by the
ActivityInterceptorwithout any developer instrumentation per endpoint. Used for quick "recently viewed" navigation and audit support.
Audiences
- Investor: Demonstrates that Dudoxx HMS goes beyond structured EHR data — the notes/tasks layer captures the informal coordination that typically leaks to personal email or WhatsApp, keeping all clinical context on-platform.
- Clinical buyer (doctor/nurse/receptionist): Dictate a voice note after a visit, get an auto-appended transcript; assign a follow-up task to a nurse with a due date; open "Recent Activity" to jump back to the last three patients you viewed.
- Developer/partner: Three independent NestJS modules under
src/misc/; each has its own controller, service(s), and Prisma models inprisma-main. Notes exposes additional sub-services for search, folders, links, attachments, and transcription. All SSE events use the sharedEventBusServiceonuser:{userId}channels. - Internal (ops/support): Activity cleanup cron runs daily at 02:00 UTC and purges records older than 90 days. Notes and tasks are tenant-scoped via
organizationIdon every Prisma query.
Architecture
Notes
NotesController → six injected services, all operating on ddx_api_main:
NotesController owns:
| Service | Responsibility |
|---|---|
NotesService | CRUD on Note model, folder + tag + link includes |
NotesSearchService | full-text search via Prisma (title, contentText, tags) |
NoteFolderService | hierarchical NoteFolder tree (recursive parentFolderId) |
NoteLinkService | polymorphic links: Note ↔ {Patient|Visit|Appointment|...} |
NotesTranscriptionService | async STT via TranscriptionService (voice-video module) |
NoteAttachmentService | upload to MinIO, trigger OCR pipeline |
NotesTranscriptionService delegates to TranscriptionService from src/voice-video/recordings/ — the same Deepgram/Gladia STT pipeline used for visit recordings. On completion it appends \n\n--- Transcription ---\n<transcript> to Note.contentText and fires NOTE_TRANSCRIPTION_COMPLETED SSE. OCR results from NoteAttachmentService are similarly appended via onOcrComplete with NOTE_OCR_COMPLETED SSE.
Tasks
| Controller | Service | Responsibility |
|---|---|---|
TasksController | TasksService | TaskGroup CRUD with items, assignees, notes (with STT), entity links |
A TaskGroup is the top-level container (title, category, priority, due date). TaskItem records are nested work items. TaskAssignee binds users to a group with a role. TaskNote supports voice recordings (STT optional). TaskLink is a polymorphic join to clinical entities (PATIENT, VISIT, APPOINTMENT, DOCUMENT, etc.) — no FK constraint, service-layer validated, mirroring the NoteLink pattern.
Activity Feed
| Component | Wiring |
|---|---|
ActivityInterceptor | global APP_INTERCEPTOR position 6 → ActivityService → prisma.recentActivity.create |
ActivityController | → ActivityService → findMany / deleteMany |
The interceptor fires post-response (via RxJS tap) on GET /patients/:id requests, writing a recentActivity row without adding latency to the response path (setImmediate). Service-account users (non-UUID IDs) are skipped silently. A daily cron at 02:00 UTC purges entries older than 90 days.
Tech Stack & Choices
| Component | Choice | Why |
|---|---|---|
| Persistence | prisma-main (ddx_api_main) | All three features share the main schema; models at notes.prisma, tasks.prisma, user-activity.prisma |
| STT for notes/tasks | TranscriptionService (voice-video module) | Re-uses the existing Deepgram/Gladia pipeline — no duplicate STT logic |
| OCR for note attachments | OcrResult model (prisma-main) linked to Attachment | Same OCR pipeline as document processing |
| Real-time updates | SSE EventBusService on user:{userId} channels | Consistent with the rest of the DDX event bus; no separate WebSocket needed |
| Note content | Json (rich-text AST) + String (contentText plain-text copy) | JSON for editor rendering; contentText for full-text search without JSON parsing |
| Activity tracking | Post-response interceptor (setImmediate) | Zero-latency cost on the critical response path |
| Task categories | TaskCategory enum (9 values) | Maps to clinical workflow stages; CUSTOM covers ad-hoc use |
Data Flow
Notes: voice dictation → transcription → SSE append
- Staff uploads audio recording linked to a note:
POST /notes/:noteId/recordings/:recordingId/transcribe. NotesTranscriptionService.startTranscriptionvalidates both theRecordingand theNoteexist (tenant-scoped Prisma queries).- SSE event
NOTE_TRANSCRIPTION_STARTEDfires immediately onuser:{userId}channel — UI shows a spinner. runTranscriptionis kicked off asynchronously (.catchlogs + firesNOTE_TRANSCRIPTION_FAILED).- On completion:
onTranscriptionCompleteappends the transcript text toNote.contentTextwith a--- Transcription ---separator; firesNOTE_TRANSCRIPTION_COMPLETED. - OCR for attachments follows the same pattern via
onOcrComplete→NOTE_OCR_COMPLETED.
Tasks: creation with assignees and entity link
POST /taskswithCreateTaskGroupDto— includes optionalitems[],assigneeIds[],links[]arrays.TasksService.createresolvesorganizationIdslug → UUID viaOrganizationResolverService.TaskGroupcreated; then items, assignees (creator auto-added as OWNER), and entity links created in follow-up transactions.- All Prisma queries carry
organizationIdfor tenant isolation.
Activity: auto-track on patient page load
- Browser navigates to patient profile → Next.js server action →
GET /patients/:id. ActivityInterceptor.interceptfires post-response:setImmediate(() => activityService.trackActivity(...)).ActivityService.trackActivityskips non-UUID user IDs (service accounts); writesrecentActivityrow withactivityType: 'viewed_patient',resourceType: 'Patient',resourceId.GET /activity/recentreturns the last 50 entries for the current user.
Implicated Code
ddx-api/src/misc/notes/notes.service.ts:39—NotesService.findAll— tenant-scoped query with folder, links, recording count, and attachment count includes; ordered pinned-firstddx-api/src/misc/notes/notes-transcription.service.ts:31—NotesTranscriptionService.startTranscription— firesNOTE_TRANSCRIPTION_STARTEDSSE immediately, runs STT asyncddx-api/src/misc/notes/notes-transcription.service.ts:104—onTranscriptionComplete— appends transcript tocontentText, firesNOTE_TRANSCRIPTION_COMPLETEDddx-api/src/misc/notes/notes-transcription.service.ts:141—onOcrComplete— appends OCR full-text fromocrResult.fullTexttocontentText, firesNOTE_OCR_COMPLETEDddx-api/src/misc/notes/notes.controller.ts:44—NotesController—@RequireClinicStaff()guard (all clinic roles),@Controller('notes')ddx-api/src/misc/tasks/tasks.service.ts:56—TasksService.create— resolves org slug to UUID, createsTaskGroup+ items + assignees + linksddx-api/src/misc/activity/activity.service.ts:22—ActivityService.trackActivity— UUID guard for service accounts,setImmediatewrite patternddx-api/src/misc/activity/activity.interceptor.ts:12—ActivityInterceptor— path regex onGET /patients/:id, post-responsetap,setImmediateddx-api/src/misc/activity/activity.service.ts:68—cleanupOldActivitycron —@Cron(EVERY_DAY_AT_2AM), deletesrecentActivityolder than 90 daysddx-api/prisma-main/models/notes.prisma:26—Notemodel —content Json?(rich-text AST) +contentText String?(plain copy for search),recordings Recording[],attachments Attachment[],links NoteLink[]ddx-api/prisma-main/models/notes.prisma:53—NoteLink— polymorphic join on(noteId, entityType, entityId)unique; no FK onentityId(service-layer validated)ddx-api/prisma-main/models/tasks.prisma:50—TaskGroup—organizationId,creatorId,category TaskCategory,status TaskStatus,priority TaskPriority,dueDate
Operational Notes
- Transcription is fire-and-forget:
startTranscriptionreturns immediately with a{ jobId }. The caller must listen forNOTE_TRANSCRIPTION_COMPLETEDorNOTE_TRANSCRIPTION_FAILEDSSE events. There is no polling endpoint. - Note
contentTextaccumulates: each transcription or OCR result is appended with a separator — it is not replaced. If a note has multiple recordings, the text grows with each--- Transcription ---block. No de-duplication mechanism exists today. - Activity interceptor is path-specific: currently wired only to
GET /patients/:id. Adding new resource types requires editingactivity.interceptor.ts:27(the path pattern check). The service is generic and accepts anyactivityType/resourceTypestring. - Task entity links have no FK:
TaskLink.entityIdis not a database foreign key — integrity is service-layer only. Deleting a linked patient/visit will not cascade-deleteTaskLinkrows. Validate on read if entity existence matters. - 90-day activity retention:
cleanupOldActivitycron atEVERY_DAY_AT_2AMis the only housekeeping. There is no per-user manual purge endpoint — onlyDELETE /activity(clear all for user). - Tenant scoping:
NotesServicealways scopes on{ organizationId: orgId, userId }— notes are private to the creating user within the org. Tasks useorganizationIdonly (multi-user collaboration). - SSE channel: all Note events use
SSEChannels.user(userId)— the personal user channel, not an org-level channel. Only the initiating user receives transcription events. - Validation commands:
pnpm tsc:check(ddx-api). If note/task schema changes:pnpm prisma:generate:allthenpnpm prisma migrate dev --schema=prisma-main --name <change>.
Related Topics
- Messenger — in-platform messaging (comparison: Messenger is multi-party; Notes are single-user scratchpads)
- SSE Event Engine —
EventBusService,SSEChannels.user(), event name constants - Prisma 7 Schemas —
prisma-mainmodel file layout;notes.prisma,tasks.prisma,user-activity.prisma - Clinical Activities — structured clinical timeline (contrast: Activity Feed here is UI navigation history, not clinical record)