Health Monitoring
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
Health Monitoring covers the clinical-tracking surface of the platform: vital-sign metrics derived from care task submissions, wound care ("Healing Ally") with AI photo analysis, DNR / advance-directive acknowledgments by caregivers, and doctors' appointments with automated reminders and AI-generated visit-history summaries. It spans three backend modules:
apps/backend/src/health-metrics/— read-side analytics over vitals task submissions + AI period reports.apps/backend/src/wound-care/— wound records, assessments, BullMQ-based AI photo analysis.apps/backend/src/clients/(subset) — DNR acknowledgments, doctors' appointments, appointment monitoring/reminders, appointment-history summaries.
Change-of-condition health observations are a sibling feature documented in 02-assessments.md and are not re-documented here.
Entities & Data Model
Vitals (no dedicated collection)
There is no vitals entity. Health metrics are computed on read from TaskSubmission documents (collection task_submissions, see 04-care-plans-and-tasks.md) whose TaskTemplate.type is one of seven vitals types (apps/backend/src/health-metrics/health-metrics.dto.ts:9-17):
WEIGHT_CHECK, BLOOD_PRESSURE_CHECK, HEART_RATE_CHECK, OXYGEN_SATURATION_CHECK, BLOOD_SUGAR_CHECK, TEMPERATURE_CHECK, RESPIRATORY_RATE_CHECK.
Only submissions with status: 'SUBMITTED' count (apps/backend/src/health-metrics/health-metrics.service.ts:84-93). Values are extracted from the submission's responses[].fieldName (e.g. systolic, diastolic, heartRateBPM, spo2, bloodGlucoseLevel, measuredWeight/weight, bodyTemperature/temperature, respiratoryRate) — health-metrics.service.ts:450-669.
HealthMetricsReport — collection health_metrics_reports
AI-assisted period report drafts (apps/backend/src/health-metrics/entities/health-metrics-report.entity.ts).
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required |
generatedBy | ObjectId → User | required |
reportType | 'single' | 'comparative' | required |
status | 'draft' | 'finalized' | default draft |
dateRange | {start, end} Dates | current period |
previousDateRange | {start, end} or null | comparative mode only |
periodReportData | Object (PeriodReportData) | full data payload built by period-report-data.service.ts |
aiSummary | Object or null | AI-generated PeriodSummary |
editedSummary | Object or null | care-manager edits over the AI summary |
recommendations | Object or null | AnayaRecommendations (care plan / hours / family-DPOA notes) |
Indexes: {client, createdAt}, {business, createdAt} (lines 95-96).
WoundRecord — collection wound_records
apps/backend/src/wound-care/entities/wound-record.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client | ObjectId → Client | required, indexed |
createdBy | ObjectId → User | required |
bodyLocation | enum BodyLocation (32 values) | required (packages/shared/src/enums/wound-care.ts:57-90) |
status | enum WoundStatus: ACTIVE, HEALING, HEALED, CHRONIC, REFERRED | default ACTIVE |
trajectory | enum WoundTrajectory: IMPROVING, STABLE, WORSENING, INDETERMINATE | default INDETERMINATE |
onsetDate, notes | Date / String | optional |
latestAssessmentId, latestAssessmentDate | ObjectId → WoundAssessment / Date | denormalized, updated atomically on each new assessment |
assessmentCount | Number | default 0, $inc'd per assessment |
healedDate | Date | set by close |
Indexes: {business, client, status}, {client, status, latestAssessmentDate}, {business, createdAt} (lines 99-101).
WoundAssessment — collection wound_assessments
apps/backend/src/wound-care/entities/wound-assessment.entity.ts
| Field | Type | Notes |
|---|---|---|
business, woundRecord, client, assessedBy | ObjectId refs | all required, first three indexed |
assessmentDate | Date | required |
photos | String[] | S3 URLs |
photoKeys | String[] | S3 keys; stripped from JSON output by the toJSON transform (line 204) — API responses get fresh signed URLs instead |
measurements | {length, width, depth, area, unit='cm'} | area auto-computed as length × width in a pre-save hook (lines 304-311) |
woundBed | {tissueType, color, percentGranulation, percentSlough, percentNecrotic} | BWAT-aligned enums |
exudate | {amount, type, color, odor} | |
edges | {description, undermining, tunneling} | |
surroundingSkin | {macerated, erythema, induration, callused, notes} | |
painLevel | Number 0–10 | |
painNotes, treatmentApplied, clinicalNotes | String | |
aiAnalysis | WoundAIAnalysis subdoc | status (PENDING/PROCESSING/COMPLETED/FAILED), measurements, clinicalDescription, tissueClassification, recommendations[], worseningIndicators[], comparisonWithPrevious, confidence, analyzedAt, errorMessage |
bwatScore | Number | exists in schema but never written by any code path (see Gaps) |
status | enum WoundAssessmentStatus: PENDING_ANALYSIS, ANALYZING, COMPLETED, ANALYSIS_FAILED | default COMPLETED |
DnrAcknowledgment — collection dnr_acknowledgments
apps/backend/src/clients/entities/dnr-acknowledgment.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
caregiver | ObjectId → User | required |
client | ObjectId → Client | required |
acknowledgedAt | Date | default now |
dnrDocumentUpdatedAt | Date | required — uploadedAt of the newest DNR/AD document at acknowledgment time; used for staleness detection |
Unique index on {caregiver, client} — one active acknowledgment per caregiver-client pair (lines 54-57); re-acknowledgment upserts the same document.
ClientDoctorsAppointment — collection client_doctors_appointments
apps/backend/src/clients/entities/client-doctors-appointment.entity.ts
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed |
client, doctor, author | ObjectId refs (Client / User / User) | doctor is a MedicalProfessional user |
startDate | Date | required |
endDate | Date | optional |
timezone | String | required |
travelTime | {hours, minutes} | required; applied symmetrically before and after the appointment when computing the blocked window (client-doctors-appointments.service.ts:1086-1098) |
transportation | enum TransportationType or null | |
alertTimings | Number[] (minutes before start) | drives the reminder cron; labels for 1440/240/120/60/30/15/10/5 in appointment.constants.ts:1-10 |
isRecurring, recurrenceId | Boolean / String (UUID) | recurrence is materialized into individual documents sharing a recurrenceId |
groupId | String | declared in schema; never written by service code |
location | Address subdoc | |
purpose, title | String | title is AI-generated from purpose, or "Dr. X - <date>" fallback (service lines 166-187) |
status | 'scheduled' | 'completed' | 'cancelled' | 'rescheduled' | default scheduled (plain strings, not a shared enum) |
isReminded | Boolean | default false; never written anywhere |
files | ObjectId[] → LockCare | pre-visit documents |
completed | subdoc | diagnosis, prescription, notes, completedBy, completedAt, files[], medications[] (AppointmentMedicationEntry), labResults, followUpInstructions, referrals, nextAppointmentDate |
cancelled | subdoc | reason, cancelledBy, cancelledAt |
rescheduled | subdoc | newStartDate, newEndDate, rescheduledBy |
AppointmentHistorySummary — collection appointment_history_summaries
apps/backend/src/clients/entities/appointment-history-summary.entity.ts
| Field | Type | Notes |
|---|---|---|
business, client, generatedBy | ObjectId refs | required |
upcomingAppointmentId | ObjectId → ClientDoctorsAppointment | optional context for the summary |
markdownSummary | String | required, AI-generated markdown |
appointmentsIncluded | ObjectId[] | the completed appointments fed to the LLM |
generatedAt | Date | required |
Workflows & State Machines
Vitals recording & analytics
- Recording happens through the care-task system, not this module: care providers submit vitals task templates (e.g. blood-pressure check) during shifts; the submission's field responses carry the readings (see 04-care-plans-and-tasks.md).
- Read APIs under
clients/:clientId/health-metrics(apps/backend/src/health-metrics/health-metrics.controller.ts):GET /— raw readings per metric for a date range, with per-readingisAbnormal+warnings, summary averages and trends.GET /recharts— same data flattened into chart points keyed by hour/day/week/month buckets, plus the threshold table (health-metrics.service.ts:182-274,1022-1046).GET /latest— most recent submission per metric type (getLatestVitals, lines 276-329).GET /trends?metric=...— single-metric trend; default window derived from period (24h / 30d / 12w / 12m, lines 1048-1070). Trend = compare first-half vs second-half averages; >±5% change ⇒increasing/decreasing, elsestable(lines 973-989).GET /abnormal— abnormal readings with severitylow|high|critical(lines 356-448).
- Abnormality thresholds are hardcoded (
health-metrics.service.ts:40-51): BP systolic 90/140/180, diastolic 60/90/110; HR 50/100/120; SpO2 <95 low, ≤90 critical; glucose 70/140/200 (plus fasting >100 rule, line 750); weight 90/300/400; temperature 96.8/99.5/103 °F (Celsius converted, lines 800-808); respiratory rate 12/20/30.
Period reports (draft → finalized)
apps/backend/src/health-metrics/health-metrics-export.service.ts:
POST reports/generate(permissionHEALTH_METRICS_MANAGE) builds a single or comparative data payload viaPeriodReportDataService(vitals + behavioral + care-delivery data,period-report-data.service.ts), then calls Anthropic (generateText, structured output) for a summary + recommendations, and saves adraftreport (lines 52-103). AI failure is caught and logged — the draft is still produced (lines 350-353).PATCH reports/:idmerges care-manager edits intoeditedSummary/recommendations; rejected if finalized (lines 109-143).POST reports/:id/finalizeflipsdraft → finalized, idempotence guarded (lines 149-165). No un-finalize path exists.GET reports/:id/exportrenders a PDF, with edits merged over the AI summary (lines 171-203).
Wound care documentation + AI analysis
Record lifecycle (apps/backend/src/wound-care/wound-care.service.ts):
POST clients/:clientId/wound-recordscreates anACTIVErecord (client must belong to the caller's business, lines 77-81) and emits a platform-log event (lines 94-104).PATCH :idcan set anyWoundStatusvia DTO (dto/update-wound-record.dto.ts:6-10) — no transition rules are enforced.PATCH :id/closesetsstatus = HEALED+healedDate = nowregardless of prior status (lines 192-220).
Assessment creation (createAssessment, lines 224-315):
- Assessment saved with manual measurements/BWAT-style observations; default
status = COMPLETED. - Wound record is updated atomically:
$inc assessmentCount,$set latestAssessmentId/latestAssessmentDate(lines 254-263). - Events emitted:
wound-care.assessment-created(notifies all business-management-role users,apps/backend/src/notification/listeners/wound-care-notification.listener.ts:27-57) and a platform log. - If at least one prior assessment exists, trajectory is recomputed in the background (lines 298-312).
Trajectory computation (computeAndUpdateTrajectory, lines 549-631) compares the two most recent assessments:
- Area change > +10% ⇒
WORSENING; < −10% ⇒IMPROVING(primary signal). - Granulation +10 points ⇒
IMPROVING(overrides area). - Necrotic +10 points ⇒
WORSENING— always wins (lines 588-594). - On
WORSENING: emitswound-care.condition-worsening(notifies management roles) + platform logWOUND_CONDITION_WORSENING.
AI photo analysis (queue wound-analysis):
- Photos are uploaded per assessment via multipart
POST .../assessments/:assessmentId/upload(S3, private, folderwound-care/{woundId}/{assessmentId}, service lines 387-414). POST .../analyzerequires ≥1 photo and rejects if status is alreadyPENDING_ANALYSIS/ANALYZING(lines 432-443); setsPENDING_ANALYSISand enqueuesanalyze-wound-photowith 2 attempts, exponential backoff 5s (lines 461-475).WoundAnalysisProcessor(apps/backend/src/wound-care/wound-analysis.processor.ts, concurrency 2, 10-min lock): setsANALYZING, downloads the first photo only (line 109), gathers up to 5 previousCOMPLETEDassessments as text summaries plus the most recent previous photo for visual comparison (lines 124-192), then callsWoundAnalysisAIService.analyzeWoundPhoto.WoundAnalysisAIService(services/wound-analysis-ai.service.ts) calls Anthropicclaude-sonnet-4-6with a BWAT-oriented system prompt and a forced tool call returning a Zod-validated structure (measurements, tissue classification, recommendations, worsening indicators, trajectory comparison, confidence). Truncated or missing tool output throws (lines 282-297).- On success:
aiAnalysissaved withstatus: 'COMPLETED', assessmentstatus → COMPLETED, eventwound-care.analysis-completednotifies only the triggering user (listener lines 59-77). On failure:aiAnalysis.status = 'FAILED'+errorMessage, assessmentstatus → ANALYSIS_FAILED(processor lines 269-277); re-trigger is allowed from that state.
Progress view: GET :id/progress returns the record, trajectory, per-assessment summaries with first-photo thumbnails, and time series for area, BWAT score and tissue composition (wound-care.service.ts:482-542).
DNR / advance-directive acknowledgment
apps/backend/src/clients/services/dnr-acknowledgment.service.ts:
- Requirement: acknowledgment is required iff
client.hasDnrOrder || client.hasAdvanceDirective(lines 55-63; flags onapps/backend/src/clients/entities/client.entity.ts:162-165). GET clients/:clientId/dnr-acknowledgment/status(no permission decorator) returns{required, acknowledged, stale};stale = truewhen the newestLockCarefile in the client'sADVANCE_DIRECTIVEfolder hasuploadedAtnewer than the storeddnrDocumentUpdatedAt(lines 69-86, 181-194).POST clients/:clientId/dnr-acknowledgment(permissionSHIFTS_EXECUTE, i.e. caregivers —clients.controller.ts:1418-1429) validates the client flags and that an AD document actually exists, then creates or refreshes the unique caregiver-client acknowledgment. Already-current acknowledgment returns{alreadyAcknowledged: true}(lines 92-149).GET clients/:clientId/dnr-acknowledgments(permissionCLIENTS_MANAGE) lists acknowledgments for admin review (lines 154-179).
Doctors' appointments
All endpoints live on ClientsController under clients/:clientId/doctors-appointments (apps/backend/src/clients/controllers/clients.controller.ts:681-905); logic in apps/backend/src/clients/services/client-doctors-appointments.service.ts.
Creation (createDoctorsAppointment, lines 152-355):
- Title is AI-generated from
purposeviaAIClientService.generateTitle, else"Dr. {name} - {date}"(lines 166-187). - Recurring appointments are expanded with
rrule(DAILY/WEEKLY/MONTHLY/YEARLY, weekday/day-of-month options, ends ON/AFTER) into individual documents sharing a randomrecurrenceId; expansion is capped at theuntildate or 1 year from start (lines 243-305). - Pre-visit files are stored as
LockCarerecords in theDOCTOR_APPOINTMENTfolder and linked on the appointment (associateFiles, lines 976-1076). - Collision check / task excusal:
POST .../check-collisionscomputes the blocked window (start − travel … end + travel), finds overlapping shift assignments, and rrule-expands the latest published care plan's task occurrences inside the window (lines 1160-1272). On create, selected occurrences are written into the care-plan tasks'excusedOccurrenceswith reasonDoctorAppointment(lines 1282-1335).
Status machine — set directly by endpoints; no transition validation exists:
- Complete (lines 431-510): stores diagnosis/prescription/notes/labs/follow-up/referrals + post-visit files + medications. Medications marked
isNewcreateClientMedicationrecords; existing ones get dosage updates — best-effort, errors swallowed (lines 480-494, 512-568). - Cancel / Reschedule (lines 710-784): write the respective subdocuments; reschedule also overwrites
startDate/endDate. - Delete is a hard delete (lines 810-822).
GET .../upcoming=startDate >= nowAND statusscheduled|rescheduled(lines 786-808).
Monitoring & reminders (apps/backend/src/clients/services/appointment-monitoring.service.ts):
- Cron every 5 minutes (line 54) scans
scheduled|rescheduledappointments starting within the next 25 hours that have non-emptyalertTimings(lines 59-71, constants inappointment.constants.ts). - For each alert timing within ±5 minutes of "minutes until start", it sends a unified notification (in-app + push) to the client's care team: management-role users in the business, accepted/assigned care providers, and active representatives (lines 96-137, 154-198). Sender is the system user.
- Dedup via
AppointmentReminderTrackingService: Redis cache keyappointment:reminder:{appointmentId}:{alertMinutes}:{userId}with 24h TTL (appointment-reminder-tracking.service.ts:9-51). Cache read errors fail open (returnsfalse⇒ may resend).
AI appointment-history summary (queue appointment-history-generation):
POST .../generate-history-summaryenqueues a job (service lines 659-680).AppointmentHistoryProcessor(apps/backend/src/clients/processors/appointment-history.processor.ts, concurrency 3) loads all completed appointments, builds a text context, and calls OpenAIgpt-4-turbo(temperature 0.3) for a 5-section markdown summary; progress and completion/failure are emitted over the notification WebSocket to thebusiness:{id}room (SocketEvents.APPOINTMENT_HISTORY_GENERATION_*, lines 92-247). Fails if the client has zero completed appointments (line 110-112).GET .../history-summary/latestreturns the newest summary;GET .../history-summary/:id/pdf-streamconverts the markdown to a PDF stream (service lines 682-708).
Business Rules & Constraints
- Vitals are derived, never stored separately — deleting/changing task submissions changes history; only
SUBMITTEDsubmissions count (health-metrics.service.ts:89). - Vitals thresholds are global constants, not per-client baselines (
health-metrics.service.ts:40-51). Temperature is normalized to Fahrenheit before comparison (lines 778-808); weight and glucose comparisons ignore the recorded unit. - Tenancy: wound-care queries always filter by
businessfrom the JWT (wound-care.service.ts:77-81, 120-125, 162-169). Doctors'-appointment reads mostly filter byclientonly (e.g.findAllAppointmentslines 357-374,findAppointmentByIdlines 376-399) — no business filter (see Gaps). - Permissions (enforced by global + controller
PermissionsGuard,apps/backend/src/auth/guards/permissions.guard.ts):- Health metrics:
HEALTH_METRICS_VIEW_ASSIGNEDfor reads,HEALTH_METRICS_MANAGEfor report generate/update/finalize (health-metrics.controller.ts:64,189,233,244). PHI access on list/report endpoints is audit-logged via@AuditPhiAccess(lines 63, 188, 224, 251). - Wound care:
WOUND_CARE_VIEW_ASSIGNEDfor reads,WOUND_CARE_MANAGEfor create/update/close/assess/upload/analyze (wound-care.controller.ts). Reads are PHI-audited. - DNR: acknowledge requires
SHIFTS_EXECUTE; admin list requiresCLIENTS_MANAGE; status check has no permission decorator (clients.controller.ts:1418-1455). - Doctors' appointments: no
@RequirePermissionson any endpoint — only the class-levelJwtAuthGuard(clients.controller.ts:92), despiteAPPOINTMENTS_*permissions existing inpackages/shared/src/enums/business-permission.ts.
- Health metrics:
- Default role grants (
packages/shared/src/constants/default-role-permissions.ts): Admin getsHEALTH_METRICS_MANAGE/WOUND_CARE_MANAGE(lines 84-88); CareProvider getsHEALTH_METRICS_VIEW_ASSIGNED,WOUND_CARE_VIEW_ASSIGNEDandWOUND_CARE_MANAGE(lines 209-211) plusSHIFTS_EXECUTE; Representative getsWOUND_CARE_VIEW_ASSIGNEDonly (line 278); MedicalProfessional getsHEALTH_METRICS_VIEW_ASSIGNED,HEALTH_METRICS_MANAGE,WOUND_CARE_VIEW_ASSIGNED(lines 337-339). - One acknowledgment per caregiver-client pair, enforced by unique index; staleness is tied to the newest
ADVANCE_DIRECTIVELockCare upload (dnr-acknowledgment.service.ts:81-85). - Wound analysis prerequisites: ≥1 photo; not already pending/analyzing (
wound-care.service.ts:432-443). Retries: 2 attempts, exponential backoff. - AI analysis never mutates the wound record —
trajectoryis computed only from manually-entered measurements on assessment creation;aiAnalysis.comparisonWithPrevious.trajectoryis stored on the assessment only. - Appointment travel time blocks symmetrically on both sides of the appointment for collision detection (
client-doctors-appointments.service.ts:1086-1098). - Reminder windows: only appointments within 25h are scanned; an alert fires when |minutes-until − alertTiming| ≤ 5; dedup key TTL 24h. A backend outage during a window silently skips that reminder — no catch-up.
- Recurring appointments are immutable as a series — the recurrence rule is not persisted, only the materialized occurrences with a shared
recurrenceId; updates/cancellations apply per-occurrence only.
Surfaces (Web & Mobile)
Web (Next.js dashboard — admins / care managers)
- Health Metrics:
apps/web/app/(app)/(admin)/dashboard/clients/[id]/edit/health-metrics/page.tsxwith chart components per vital (_components/*-chart.tsx), latest vitals, abnormal readings, and period-report list/detail (health-reports-list.tsx,health-report-detail.tsx). Data hooks inapps/web/hooks/use-health-metrics.tscall all backend endpoints including reports. - Wound Care: list page
.../edit/wound-care/page.tsx, record detail.../wound-care/[woundId]/page.tsx, assessment detail.../assessments/[assessmentId]/page.tsx; API clientapps/web/lib/wound-care-api.ts(create/update/close records, create assessments, upload photos, trigger analysis, progress). - Doctors' Appointments:
.../edit/doctors-appointments/page.tsxwith calendar, create/edit dialogs, complete/cancel/reschedule forms (_components/), plus a business-wide calendar view (dashboard/calendar/_components/appointment-detail-dialog.tsx). - DNR: the client layout shows a DNR indicator from
clientQuery.data.hasDnrOrder(apps/web/app/(app)/(admin)/dashboard/clients/[id]/layout.tsx:186). No web UI was found that calls the acknowledgment endpoints (status, acknowledge, or the admin list).
Mobile (Expo — care providers / families)
- Health (vitals):
apps/mobile/app/(client)/[id]/health/index.tsxshows latest vitals plus 30-day trend line charts (heart rate, systolic BP, SpO2, temperature) usinguseLatestVitals/useHealthMetricsRecharts(apps/mobile/hooks/use-health-metrics.ts). Vitals recording happens through care-task submission screens (sibling module). - Wound Care: full flow — list (
wound-care/index.tsx), create record (create.tsx), record detail ([woundId]/index.tsx), new assessment (new-assessment.tsx), assessment detail (assessments/[assessmentId].tsx); API clientapps/mobile/lib/wound-care-api.tsincluding photo upload andtriggerAnalysis. Care providers can do this because they holdWOUND_CARE_MANAGEby default. - Appointments:
apps/mobile/lib/appointments-api.tscovers list/upcoming/detail/create/complete/cancel/reschedule and doctor file albums. - DNR: client screens render a passive
AdvanceDirectiveBannerwhenhasAdvanceDirective || hasDnrOrder(apps/mobile/app/(client)/[id]/index.tsx:124,care-plan/index.tsx:282,apps/mobile/components/ui/advance-directive-banner.tsx).apps/mobile/lib/client-api.ts:16-32definesgetDnrAcknowledgmentStatus/acknowledgeDnr, but no screen invokes them (frontend-only banner; acknowledgment flow appears unwired — see Gaps).
Frontend-only rules: chart bucketing windows (mobile 30-day fixed window), and any role-based hiding of wound-care/appointment actions are UI conveniences — the backend enforces only the permissions listed above (and none for appointments).
Cross-Module Dependencies
- health-monitoring → care tasks: vitals are read from
TaskSubmission/TaskTemplate(health-metrics.service.ts:53-58); appointment creation excuses care-plan task occurrences inCareProviderTasks(client-doctors-appointments.service.ts:1282-1335). See 04-care-plans-and-tasks.md. - health-monitoring → scheduling/shifts: collision detection queries
ShiftAssignment(client-doctors-appointments.service.ts:1174-1186); DNR acknowledgment is gated bySHIFTS_EXECUTE. See 05-scheduling-and-shifts.md. - health-monitoring → notifications: wound-care events consumed by
WoundCareNotificationListener(apps/backend/src/notification/listeners/wound-care-notification.listener.ts); appointment reminders and history-generation progress go throughNotificationService/NotificationGatewayWebSocket rooms. - health-monitoring → files (LockCare/S3): wound photos via
FilesService(signed URLs); appointment pre/post-visit files and advance-directive documents areLockCarerecords (lock-care.service.tsusage inclient-doctors-appointments.service.ts:976-1076,dnr-acknowledgment.service.ts:181-194). - health-monitoring → AI services: Anthropic via Vercel AI SDK for wound analysis (
wound-analysis-ai.service.ts:263-280) and period-report summaries (health-metrics-export.service.ts:43-46); OpenAI for appointment-history summaries (appointment-history.processor.ts:59-62);AIClientService.generateTitlefor appointment titles. - health-monitoring → medications: completing an appointment syncs prescribed medications into
ClientMedication(client-doctors-appointments.service.ts:512-568). See 01-clients.md. - health-monitoring → platform logs: wound record/assessment actions and appointment CRUD emit
PLATFORM_LOG_ACTIVITY_EVENT; PHI reads are audited via@AuditPhiAccess. - Sibling: change-of-condition health observations — 02-assessments.md.
Open Questions & Gaps
- DNR acknowledgment flow appears unwired on the frontends. The backend implements status/acknowledge/list endpoints and the mobile API client defines
acknowledgeDnr/getDnrAcknowledgmentStatus(apps/mobile/lib/client-api.ts:16-32), but no mobile screen or web page calls them — only passive banners exist. The service docstring mentions a "clock-in flow" (dnr-acknowledgment.service.ts:45-46), yet nothing inapps/backend/src/shifts/references DNR. Whether acknowledgment was meant to gate clock-in cannot be determined from code. - Doctors'-appointment endpoints have no permission checks and weak tenant scoping. No
@RequirePermissionsdespiteAPPOINTMENTS_*permissions existing; several reads/writes (findAllAppointments,findAppointmentById,updateAppointment,deleteAppointment) filter only byclientId/appointmentIdwith nobusinessfilter — any authenticated user with a valid client id can access another tenant's appointments (globalBusinessGuardonly checks that the caller's own business is active,apps/backend/src/common/guards/business.guard.ts:20-26). statusCountsaggregation infindAllWoundRecordslikely always returns empty. The$matchuses raw stringbusinessId/clientIdagainst ObjectId fields (wound-care.service.ts:136-139); unlikefind(), aggregation pipelines do not auto-cast strings to ObjectIds. Cannot confirm without runtime data, but the types disagree.bwatScoreis never written. It exists onWoundAssessment, is absent fromCreateWoundAssessmentDto, and is charted in the progress endpoint (wound-care.service.ts:516-521) — the BWAT-score series will always be empty.- AI analysis results don't feed trajectory. The AI returns
comparisonWithPrevious.trajectory, butWoundRecord.trajectoryis only updated from manual measurements at assessment-creation time; AI completion neither recomputes trajectory nor emits worsening events even whenworseningIndicatorsare returned. - Wound status has no state machine.
UpdateWoundRecordDtoaccepts anyWoundStatus;closealways forcesHEALEDregardless of current state;HEALING/CHRONIC/REFERREDhave no defined transitions or behavior anywhere in backend logic. - Vitals checks are unit-naive. Weight thresholds (90/300/400) are applied to the raw number regardless of the recorded unit (
lbsdefault butkgpossible,health-metrics.service.ts:579-603, 761-776); blood glucose ignoresmmol/Lvsmg/dL. ThesupplementalOxygenboolean is derived such that any value other than exactly'No'evaluates true (lines 529-533). - Summary metadata omits two metrics.
getAvailableMetrics,calculateAveragesandcalculateTrendsskip temperature and respiratory rate entirely (health-metrics.service.ts:893-971, 1072-1083), andgetAbnormalReadingsnever reports abnormal temperature/respiratory-rate readings (lines 368-433) even though per-reading flags are computed. - Reminder delivery is best-effort. ±5-minute window on a 5-minute cron means a missed cycle (deploy/outage) silently drops that alert; Redis dedup errors fail open and may double-send (
appointment-reminder-tracking.service.ts:27-36).isRemindedon the appointment entity is dead — never set. - Recurring appointments: the rrule is not persisted (only materialized occurrences +
recurrenceId), so series-level edit/cancel is impossible; expansion caps at 1 year with no rolling extension;associateFilesis called per occurrence, creating duplicateLockCarerecords for the same uploaded file across occurrences (client-doctors-appointments.service.ts:243-305).groupIdis never written. rescheduledkeeps only the latest reschedule (single subdocument, overwritten each time), and there is no guard preventing completing/cancelling an already-completed or cancelled appointment —statusis overwritten unconditionally.- Mixed AI providers with different failure semantics: wound analysis and period reports use Anthropic (period-report AI failures are swallowed, draft still saved); appointment-history summaries use OpenAI
gpt-4-turbo(failure fails the whole job). Whether this split is intentional cannot be determined from code. - DNR staleness only tracks the newest AD document — if a client has
hasDnrOrdertrue but no file in theADVANCE_DIRECTIVEfolder, status reportsrequiredyetacknowledgerejects with "No advance directive document found", leaving caregivers unable to acknowledge (dnr-acknowledgment.service.ts:108-113). - Wound analysis examines only the first photo of an assessment (
wound-analysis.processor.ts:109); additional uploaded photos are stored but never analyzed.