# VigilCare Records Platform — Gap Analysis Comprehensive gap analysis of the VigilCare Records digitization system covering data integrity, infrastructure reliability, security posture, API completeness, Vue frontend coverage, observability, and test coverage. Analysis compares the current implementation against the [PRD](vigilcare-records-prd.md) and identifies issues ordered by **impact on data correctness and clinical safety first**, then **operational reliability**, then **feature completeness**, then **observability and polish**. Each item includes **why** it matters and **how** to fix it at an implementation-ready level. --- ## Priority legend | Tier | Meaning | |------|---------| | **P0** | Data integrity or correctness bug; fix before expanding production usage | | **P1** | Silent misconfiguration, transaction safety hole, or data loss risk; fix soon after P0 | | **P2** | Blocks common workflows or degrades operational reliability | | **P3** | Security hardening, compliance, or defense-in-depth; no immediate patient-safety impact | | **P4** | API/UI completeness, consistency, and developer/operator experience | | **P5** | Observability and test coverage; does not change outcomes but makes incidents diagnosable | --- # Part A — Data Integrity & Correctness --- ## P0 — PromoteAsync (retry path) does not create clinical entities ### Problem `PromotionService` has two code paths for promotion: 1. `ApproveAndPromoteAsync` (called by `POST .../approve`) — creates clinical `Patient`, `Encounter`, `Observation` entities **plus** `LiveEncounter`, `LiveObservation`, and `OutboxEvent` entries. This is the happy path. 2. `PromoteAsync` (called by `POST .../promote` and `PromotionRetryService`) — creates only `LiveEncounter` and `LiveObservation` entries. It does **not** create clinical `Patient`, `Encounter`, or `Observation` records, and does **not** write `OutboxEvent` entries. When initial promotion fails and the batch is deferred to `APPROVED` status (202 response), the `PromotionRetryService` retries via `PromoteAsync`. On success, the batch is marked `PROMOTED` but the clinical tables that VigilCareClinical depends on are never populated. The batch appears promoted, but observations are invisible to the downstream alert pipeline, ward dashboard, and scoring consumers. ### Why fix A successfully retried promotion that fails to populate clinical tables defeats the entire purpose of the digitization pipeline. Observations from deferred promotions never reach VigilCareClinical's alert engine — critical values entered during a live capture that was deferred due to transient infrastructure failure will silently disappear from clinical monitoring. ### How to fix 1. **Unify the promotion logic**: `PromoteAsync` should call the same core promotion method as `ApproveAndPromoteAsync`, with the only difference being that approval-specific validation (status check for `Verified`/`AwaitingClinicalApproval`, separation-of-duties) is already done. 2. Extract a shared `ExecutePromotionAsync(batch, actorUserId, enableRetroactiveAlerts)` method that creates Patient, Encounter, Observation, LiveEncounter, LiveObservation, and OutboxEvent in a single transaction. 3. Both `ApproveAndPromoteAsync` and `PromoteAsync` should call this shared method. 4. Ensure the batch's `EnableRetroactiveAlerts` flag (set during `DeferPromotionAsync`) is read and passed to the shared method on retry. 5. Add an integration test: defer promotion (simulate infra failure) → retry succeeds → verify clinical Observation rows exist. **Files:** `PromotionService.cs:529-658` (PromoteAsync), `PromotionService.cs:24-220` (ApproveAndPromoteAsync). **Dependency:** None. --- ## P0 — Patient deduplication by exact name + DOB is fragile ### Problem `PromotionService.CreateOrUpdatePatientAsync` matches existing patients by exact `FullName` string equality and `DateOfBirth`. Two batches for the same physical patient with different name representations ("Maria Santos" vs "MARIA SANTOS" vs "Maria R. Santos" vs "Santos, Maria") create duplicate patient records with distinct MRNs. ### Why fix MRN is the primary patient identifier. Duplicate patients split their clinical history across multiple MRNs — observations, encounters, and alerts for the same person appear under different identities. This fragments the clinical picture and can lead to missed critical trend alerts (e.g., three potassium readings spread across two patient records don't trigger a trend). ### How to fix 1. **Normalize name comparison**: case-insensitive, whitespace-trimmed comparison as minimum. Consider `ToUpperInvariant().Trim()` normalization. 2. **Add fuzzy matching warning**: if no exact match but a close match exists (e.g., Levenshtein distance < 3 on normalized name + exact DOB match), log a warning and return the match with a flag in the promotion result indicating a fuzzy match was used. 3. **Add a patient merge endpoint** (future): `POST /api/v1/patients/{targetId}/merge/{sourceId}` for administrator-driven deduplication after the fact. 4. **Short term**: at minimum, normalize the comparison to case-insensitive with trimming. **Files:** `PromotionService.cs:266-295` (CreateOrUpdatePatientAsync). **Dependency:** None. --- ## P1 — Batch assignment creates inconsistent interim state ### Problem `BatchService.AssignAsync` sets `EnteredByUserId` on a batch in `UPLOADED` status and acquires a Redis lock, but does **not** transition the batch to `IN_ENTRY` status. The status transition only happens later when `DraftService` processes the first draft save. Between assignment and first save, the batch is in `UPLOADED` status with an assigned user — a state not represented in the PRD's status machine. If the Redis lock expires (1-hour TTL) before the clerk saves any draft, another assignment call could succeed but the original `EnteredByUserId` is already set on the batch row. The second `AssignAsync` would set a new `EnteredByUserId` without clearing the Redis lock from the first assignment (the lock already expired). ### Why fix The entry work queue (`GET /api/v1/work-queue/entry`) queries for batches in `UPLOADED`, `IN_ENTRY`, and `REJECTED` statuses. A batch in `UPLOADED` status with `EnteredByUserId` set appears in the entry queue as if it's being worked on, but the status suggests it hasn't been started. Supervisors cannot distinguish between "assigned but not started" and "not yet assigned" from the status alone. ### How to fix 1. **Transition to `IN_ENTRY` during assignment**: `AssignAsync` should set `batch.Status = BatchStatus.InEntry` immediately after acquiring the Redis lock and setting `EnteredByUserId`. 2. **Remove the implicit transition in DraftService**: `DraftService` currently transitions `UPLOADED → IN_ENTRY` on first save. Since assignment now does this, the DraftService check becomes redundant (keep the `IN_ENTRY` status check as a guard). 3. **Handle Redis lock expiry**: add a `PATCH .../reassign` endpoint (or extend `AssignAsync` for `IN_ENTRY` batches) that clears the old Redis lock, acquires a new one, updates `EnteredByUserId`, and writes a `DigitizationEvent` for reassignment. 4. Validate that the work queue entry view only shows `IN_ENTRY` and `REJECTED` batches for the entry clerk, not `UPLOADED`. **Files:** `BatchService.cs:192-229` (AssignAsync), `DraftService.cs` (first-save transition logic). **Dependency:** None. --- ## P1 — No duplicate detection on concurrent batch creation ### Problem `BatchService.CreateAsync` checks for duplicate documents by querying `DocumentSha256` for the same patient within 24 hours. However, two concurrent upload requests with the same file for the same patient can both pass the `AnyAsync` check before either commits. No database-level unique constraint on `(DocumentSha256, PatientId)` with a time window exists. ### Why fix FHIR integration engines or intake automation scripts submitting the same scan file simultaneously could create duplicate batches. While not a clinical safety issue (duplicate entry would be caught at verification), it wastes verifier time and clutters the work queue. ### How to fix 1. Add a database-level unique partial index: `CREATE UNIQUE INDEX IX_batch_sha256_patient_24h ON digitization_batches (document_sha256, patient_id) WHERE created_at >= NOW() - INTERVAL '24 hours'` — note: PostgreSQL partial indexes with volatile expressions are not directly supported; instead, use an advisory lock or a dedicated deduplication table. 2. Alternative: use a Redis `SET NX` with key `batch:dedup:{sha256}:{patientId}` and 24-hour TTL as a first-line check. The database `AnyAsync` remains as a fallback. 3. Wrap the upload + batch insert in a serializable transaction scope for the dedup check. **Files:** `BatchService.cs:71-85` (duplicate detection), `DigitizationBatchConfiguration.cs` (add index). **Dependency:** None. --- # Part B — Infrastructure & Reliability --- ## P2 — No health check endpoints ### Problem The API has no `/health`, `/ready`, or `/startup` endpoints. There are no health checks for PostgreSQL, Redis, or MinIO connectivity. ### Why fix Container orchestrators (Docker Compose health checks, Kubernetes probes) cannot detect unhealthy instances. If PostgreSQL or Redis becomes unreachable after startup, the API continues accepting requests that will fail with 500 errors. Load balancers cannot route traffic away from degraded instances. ### How to fix 1. Add `Microsoft.Extensions.Diagnostics.HealthChecks` and provider packages: - `AspNetCore.HealthChecks.NpgSql` (PostgreSQL) - `AspNetCore.HealthChecks.Redis` (Redis) 2. Create a custom `MinioHealthCheck` that calls `BucketExistsAsync`. 3. Register health checks in `Program.cs` with tags: `startup`, `liveness`, `readiness`. 4. Map endpoints: - `GET /health/live` — is the process alive? - `GET /health/ready` — are PostgreSQL, Redis, and MinIO reachable? - `GET /health/startup` — has migration and seed completed? 5. Add health check responses to Prometheus via `AspNetCore.HealthChecks.Publisher.Prometheus`. 6. Update `docker-compose.yml` with health check configuration on the API service. **Files:** `Program.cs`, `VigilCareRecordsAPI.csproj`, optional `MinioHealthCheck.cs`. **Dependency:** None. --- ## P2 — No CORS configuration for production deployment ### Problem The API has no CORS configuration. The Vue frontend works in development via Vite's proxy (`/api → localhost:5217`), but in production where the frontend is served from a different origin (e.g., `https://records.vigilcare.local` vs `https://api.vigilcare.local`), API requests will be blocked by the browser's same-origin policy. ### Why fix Without CORS, the digitization workstation cannot call the API in any deployment topology where the frontend is served from a different domain, port, or protocol than the API. This blocks every non-dev deployment. ### How to fix 1. Add CORS configuration to `Program.cs`: ```csharp builder.Services.AddCors(options => { options.AddPolicy("VigilCare", policy => { policy.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get()!) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); ``` 2. Add `app.UseCors("VigilCare")` before `app.UseAuthentication()`. 3. Add `Cors:AllowedOrigins` to `appsettings.json` (default: `["http://localhost:3028"]` for dev). 4. Document production CORS configuration in README. **Files:** `Program.cs`, `appsettings.json`. **Dependency:** None. --- ## P2 — Redis connection failure crashes startup ### Problem `Program.cs` calls `ConnectionMultiplexer.Connect(...)` synchronously during DI registration. If Redis is unreachable at startup, this throws an unhandled `RedisConnectionException` that crashes the application. There is no retry logic. ### Why fix Redis is used for batch assignment locks and alert threshold caching — important but not essential for core API functionality. A transient Redis outage during deployment or container restart should not prevent the entire API from starting. The batch assignment lock is a convenience feature; the separation-of-duties enforcement at the service layer is the true guard. ### How to fix 1. Replace synchronous connection with lazy initialization: ```csharp builder.Services.AddSingleton(sp => { var config = ConfigurationOptions.Parse( sp.GetRequiredService()["Redis:ConnectionString"]!); config.AbortOnConnectFail = false; return ConnectionMultiplexer.Connect(config); }); ``` 2. Setting `AbortOnConnectFail = false` allows the multiplexer to be created even if Redis is unreachable — operations will fail gracefully until Redis recovers. 3. In `BatchService.AssignAsync`, catch `RedisConnectionException` and log a warning; optionally fall back to a database-level advisory lock. 4. In `LiveCaptureService`, handle Redis cache miss for thresholds (already falls back to database query). **Files:** `Program.cs:26-27`, `BatchService.cs:204`. **Dependency:** None. --- ## P2 — PromotionRetryService does not record failure metrics ### Problem `PromotionRetryService` logs errors on retry failure but does not increment any Prometheus counter. There is no metric for retry attempts, retry successes, retry failures, or retries exhausted. The existing `DiagnosticsMetrics` class does not have any retry-related metrics. ### Why fix Operators monitoring Prometheus/Grafana have no visibility into whether deferred promotions are succeeding or failing on retry. A sustained promotion failure (e.g., due to a configuration issue) would only be visible in Seq logs, not in dashboards or alerts. This delays incident detection. ### How to fix 1. Add metrics to `DiagnosticsMetrics`: ```csharp public static readonly Counter PromotionRetryTotal = Metrics.CreateCounter( "digitization_promotion_retry_total", "Total promotion retry attempts.", new CounterConfiguration { LabelNames = new[] { "outcome" } }); // success, failure, exhausted ``` 2. In `PromotionRetryService.RetryPromotionAsync`: - On success: `DiagnosticsMetrics.PromotionRetryTotal.WithLabels("success").Inc()`. - On failure: `DiagnosticsMetrics.PromotionRetryTotal.WithLabels("failure").Inc()`. - On exhausted: `DiagnosticsMetrics.PromotionRetryTotal.WithLabels("exhausted").Inc()`. 3. Add a gauge for pending retry count: `digitization_promotion_pending_retries` (update in MetricsCollectorService). **Files:** `DiagnosticsMetrics.cs`, `PromotionRetryService.cs:117-175`, `MetricsCollectorService.cs`. **Dependency:** None. --- # Part C — Security & Hardening --- ## P3 — JWT signing key not validated on startup ### Problem `Program.cs` reads `JwtOptions.Secret` from configuration and creates a `SymmetricSecurityKey` without validating minimum length. A short or empty key causes a runtime `SecurityTokenInvalidSignatureException` on the first authentication attempt, not at startup. The dev default key (`VigilCareRecordsDevSecretKeyAtLeast32Chars!`) is 43 characters, which is sufficient, but nothing prevents a production deployment from using a shorter key. ### Why fix Fail-fast on misconfiguration prevents deploying a service that silently rejects all authentication. In production, a weak key means all JWTs can be forged by an attacker who brute-forces the HMAC. ### How to fix 1. Add a startup validation check in `Program.cs` after binding `JwtOptions`: ```csharp if (string.IsNullOrEmpty(jwtOptions.Secret) || Encoding.UTF8.GetByteCount(jwtOptions.Secret) < 32) throw new InvalidOperationException( "JWT Secret must be at least 256 bits (32 bytes)."); ``` 2. Optionally implement `IValidateOptions` for structured validation. **Files:** `Program.cs:45-58`, optionally `JwtOptions.cs`. **Dependency:** None. --- ## P3 — No rate limiting on authentication endpoints ### Problem The `POST /api/v1/auth/login` endpoint has no rate limiting. An attacker can attempt unlimited password guesses against known usernames (seeded users have predictable usernames: `intake1`, `entry1`, `verifier1`, etc.). There is no account lockout mechanism after failed attempts. ### Why fix The API handles PHI (Protected Health Information). Brute-force attacks on auth endpoints are a standard OWASP risk. With all seeded users sharing the password `password`, a single guess compromises any account. ### How to fix 1. **Rate limiting**: Add `AspNetCoreRateLimit` or the built-in .NET 7+ `AddRateLimiter()`: ```csharp builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("auth", opt => { opt.Window = TimeSpan.FromMinutes(5); opt.PermitLimit = 10; opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; }); }); ``` 2. Apply `[EnableRateLimiting("auth")]` to login and refresh endpoints. 3. **Account lockout**: Add `FailedLoginAttempts` and `LockedUntil` columns to `User`. After 5 failed attempts in 15 minutes, lock the account for 30 minutes. 4. Return `429 Too Many Requests` on rate limit, `423 Locked` on account lockout. **Files:** `Program.cs`, `AuthController.cs`, `AuthService.cs`, `User.cs`, migration. **Dependency:** None. --- ## P3 — Credentials stored in plaintext in appsettings.json ### Problem `appsettings.json` contains plaintext credentials committed to source control: - JWT signing key: `VigilCareRecordsDevSecretKeyAtLeast32Chars!` - PostgreSQL password: `password` - MinIO access/secret keys: `minioadmin`/`minioadmin` - Redis connection string (no auth) ### Why fix Anyone with repository access can extract production credentials if the same configuration pattern is used in deployment. Even for development, committed secrets create a precedent that normalizes insecure practices. ### How to fix 1. Move all secrets to environment variables or a secrets manager. 2. Add `appsettings.Development.json` with dev-only defaults (already gitignored by convention). 3. Add `appsettings.Production.json.example` documenting required secrets. 4. Document environment variable overrides: - `ConnectionStrings__DefaultConnection` - `Jwt__Secret` - `Minio__AccessKey`, `Minio__SecretKey` 5. Add a `.env.example` file for docker-compose that documents required secrets without values. 6. Consider Azure Key Vault or HashiCorp Vault integration for production. **Files:** `appsettings.json`, new `.env.example`, `docker-compose.yml`. **Dependency:** None. --- ## P3 — No audit of document access ### Problem The PRD explicitly requires logging "who viewed a scan and when" as an audit requirement (Section 9: Authentication and Audit). The current implementation generates presigned URLs in `BatchService.GetByIdAsync` and `DocumentStorageService.GetPresignedUrlAsync` without recording who requested the URL or when. ### Why fix HIPAA and clinical compliance audits require demonstrating that access to PHI (including scanned patient documents) is logged. Without document access logging, there is no evidence of who viewed which patient's scan, making the system non-compliant with the PRD's stated audit requirements. ### How to fix 1. Add a `DigitizationEventType.DocumentAccessed` enum value. 2. In `DigitizationBatchesController.GetById`, after generating the presigned URL, write a `DigitizationEvent`: ```csharp _db.DigitizationEvents.Add(new DigitizationEvent { Id = Guid.NewGuid(), BatchId = id, EventType = DigitizationEventType.DocumentAccessed, ActorUserId = userId, OccurredAt = DateTimeOffset.UtcNow, MetadataJson = JsonSerializer.Serialize(new { expiresAt = presignedUrlExpiry }) }); ``` 3. Consider rate-limiting audit writes: if the same user accessed the same batch within the last 5 minutes, skip the duplicate event (prevents audit noise during entry/verification when the page auto-refreshes the URL). **Files:** `DigitizationBatchesController.cs`, `DigitizationEventType.cs`, migration (enum update). **Dependency:** None. --- # Part D — API Completeness & Consistency --- ## P2 — No FluentValidation for request DTOs ### Problem The API relies entirely on inline validation in service methods and controllers. There are no `FluentValidation` validators for any of the 15+ request DTOs (`CreateBatchRequest`, `UpsertDraftPatientRequest`, `UpsertDraftEncounterRequest`, `CreateDraftObservationRequest`, `VerifyBatchRequest`, `RejectBatchRequest`, `ApproveRequest`, `RecordObservationsRequest`, `OpenEncounterWithVitalsRequest`, `LoginRequest`, etc.). Invalid request bodies reach the service layer before being rejected, which means: - Missing required fields cause `NullReferenceException` instead of 422 responses. - String length violations hit database constraints instead of validation errors. - Validation error messages are inconsistent across endpoints. ### Why fix Consistent request validation is essential for integration reliability. FHIR integration engines, the Vue frontend, and CLI scripts all need predictable error shapes when submitting invalid data. Database constraint violations surfacing as 500 errors instead of 422s make debugging integration issues significantly harder. ### How to fix 1. Add `FluentValidation.AspNetCore` package. 2. Create validators for all request DTOs. Priority validators: - `LoginRequestValidator`: username required, password required (min 1 char) - `UpsertDraftPatientRequestValidator`: fullName max 200, DOB not future - `UpsertDraftEncounterRequestValidator`: admissionDate not future, department valid enum - `CreateDraftObservationRequestValidator`: observationCode required, value required, recordedAt required and not future - `VerifyBatchRequestValidator`: fieldChecks non-empty, each status is valid - `RejectBatchRequestValidator`: reason required, min 10 chars (already enforced in service) - `RecordObservationsRequestValidator`: observations non-empty, max 10, each has code/value/unit/recordedAt 3. Register validators with auto-discovery: `builder.Services.AddValidatorsFromAssemblyContaining()`. 4. Add `app.UseFluentValidationExceptionHandler()` or integrate with the existing `ExceptionHandlerMiddleware`. **Files:** New `Validators/` directory, `Program.cs`, `VigilCareRecordsAPI.csproj`. **Dependency:** None. --- ## P4 — No user management endpoints (create, update, deactivate) ### Problem `UsersController` has only `GET /api/v1/users?role=` to list users. There are no endpoints to create users, update user details, change passwords, or deactivate accounts. User management is only possible through the `DataSeeder` or direct database access. The PRD assigns the Administrator role permission for "User management, batch type config, retroactive alert policy, work-queue reassignment." ### Why fix In a production deployment, new staff must be onboarded (new data entry clerks, verifiers) and departing staff must be deactivated. Without user management endpoints, every personnel change requires direct database access and a service restart to re-seed, which is unacceptable for a clinical system. ### How to fix 1. Add endpoints to `UsersController`: - `POST /api/v1/users` — create user (admin only): username, password, fullName, role - `PATCH /api/v1/users/{id}` — update user details: fullName, role, isActive - `POST /api/v1/users/{id}/reset-password` — admin password reset - `POST /api/v1/users/{id}/change-password` — self-service (current + new password) 2. Create `UserService` with `CreateAsync`, `UpdateAsync`, `ResetPasswordAsync`, `ChangePasswordAsync`. 3. Add `CreateUserRequest`, `UpdateUserRequest`, `ChangePasswordRequest` DTOs with FluentValidation validators. 4. Enforce password complexity: min 8 chars, at least 1 uppercase, 1 digit. 5. Log user management actions as `AuthAuditEvent` entries. **Files:** `UsersController.cs`, new `UserService.cs`, new DTOs in `Models/Records/User/`. **Dependency:** None. --- ## P4 — No batch cancel/void operation ### Problem Once a batch is created, it can only move forward through the status machine or be rejected (which returns it to entry). There is no way to permanently cancel or void a batch that was created in error (wrong patient, wrong batch type, test upload). The status machine has no terminal state other than `PROMOTED`. ### Why fix Intake clerks create batches by uploading scans. Mistakes happen: wrong document scanned, wrong patient linked, test uploads during training. Without a cancel operation, these batches permanently occupy the work queue, cluttering the entry and verification views. Supervisors must use direct database access to clean up. ### How to fix 1. Add `BatchStatus.Cancelled` as a terminal state (alongside `Promoted`). 2. Add allowed transitions: `UPLOADED → CANCELLED`, `IN_ENTRY → CANCELLED`, `REJECTED → CANCELLED`. 3. Add `POST /api/v1/digitization-batches/{id}/cancel` — body: `{ "reason": "..." }` — restricted to `ADMINISTRATOR` role. 4. Write `DigitizationEvent` with `EventType.Cancelled` and the reason. 5. Release Redis assignment lock on cancellation if one exists. 6. `CANCELLED` batches are excluded from work queue queries. **Files:** `BatchService.cs` (add allowed transitions), `DigitizationBatchesController.cs`, `BatchStatus.cs`, `DigitizationEventType.cs`, migration. **Dependency:** None. --- ## P4 — Pagination missing `sortBy` and `sortDirection` parameters ### Problem All list endpoints (`GET /digitization-batches`, work queue endpoints) sort by `CreatedAt DESC` or `UpdatedAt ASC` with no user-configurable sort. The supervisor dashboard cannot sort batches by status, type, patient, or age. ### Why fix Supervisors managing a work queue of 50+ batches need to sort by different criteria: oldest first for urgency, by type for batch processing, by assigned clerk for workload review. The Vue frontend's QueueDashboardView shows all batches in a flat list with no sort controls. ### How to fix 1. Add `sortBy` and `sortDirection` query parameters to `GET /api/v1/digitization-batches` and all work queue endpoints. 2. Supported sort fields: `createdAt`, `updatedAt`, `status`, `batchType`, `track`. 3. Default: `createdAt DESC`. 4. Validate sort field against allowed list; reject unknown fields with 422. 5. Update `BatchService.ListAsync` and `WorkQueueService` to apply dynamic ordering. **Files:** `BatchService.cs:167-190`, `WorkQueueService.cs`, `DigitizationBatchesController.cs`, `WorkQueueController.cs`. **Dependency:** None. --- # Part E — Vue Frontend Gaps --- ## P2 — No clinical approval view ### Problem The PRD defines a distinct clinical approval step for high-stakes batch types (vitals, labs, encounter summaries, medications, mixed). The backend implements `GET /api/v1/work-queue/clinical-approval` and `POST .../approve` endpoints. However, the Vue frontend has no dedicated view for clinical approvers. The router assigns `CLINICAL_APPROVER` to the verification routes, meaning approvers use the same `VerificationView` that verifiers use. The `VerificationForm` component has approve/reject buttons but calls `verifyBatch()` (which posts to `POST .../verify`), not `approveBatch()` (which posts to `POST .../approve` with an `Idempotency-Key`). There is no UI for the `AWAITING_CLINICAL_APPROVAL` queue. ### Why fix Clinical approvers (typically physicians) have a different workflow from verifiers. They are not comparing data entry against a scan — they are authorizing promotion of clinical data into the live system. Conflating the two roles in one view means clinical approval batches either sit in the verification queue unnoticed or are processed using the wrong action. ### How to fix 1. Add `ApprovalView.vue` at `/approval` route, accessible to `CLINICAL_APPROVER` and `ADMINISTRATOR`. 2. The view loads batches from `GET /api/v1/work-queue/clinical-approval`. 3. Each batch shows: patient summary, encounter context, observations (read-only), verification pass details, verifier name. 4. Approve button calls `POST .../approve` with a generated `Idempotency-Key` header. 5. Reject button calls `POST .../reject` with reason. 6. Show promotion result (patient MRN, encounter ID, observation count) on success. 7. Handle 202 `PROMOTION_DEFERRED` — show a banner: "Approved. Promotion will be retried automatically." 8. Add the route to the router with `meta.roles: ['CLINICAL_APPROVER', 'ADMINISTRATOR']`. 9. Update `AppHeader.vue` navigation to include the approval link. **Files:** New `views/ApprovalView.vue`, `router/index.ts`, `stores/batches.ts` (add `approveBatch` action), `components/AppHeader.vue`. **Dependency:** None. --- ## P2 — No live capture view for bedside data entry ### Problem The PRD describes Track B live capture as a core feature: "Credentialed clinician enters vitals or labs at point of care on a tablet." The backend implements `POST /api/v1/live-capture/encounters/{encounterId}/observations` and `POST /api/v1/live-capture/encounters` with clinician attestation and synchronous critical alert evaluation. The Vue frontend has no live capture view. The router has no `/live-capture` route. Clinicians (`CLINICIAN` role) can log in but are redirected to `/login` because there is no default route for their role, and no route allows `CLINICIAN` access. ### Why fix Live capture is the path from paper-to-digital for current patient care. Without a bedside UI, clinicians cannot use the system for real-time vital sign entry — the primary workflow that makes VigilCare actionable in a clinical setting. The backend is fully implemented but inaccessible through the frontend. ### How to fix 1. Add `LiveCaptureView.vue` at `/live-capture`, accessible to `CLINICIAN` and `ADMINISTRATOR`. 2. The view provides: - Patient search (reuse `PatientSearch` component) - Active encounter selector (or create-new-encounter flow) - Compact vitals entry form (optimized for tablet): - Observation rows for standard codes (HR, Temp, BP, RR, SpO2) - Password re-confirmation field (PRD requires password or PIN) - Clinician attestation checkbox - Submit button that calls `POST /api/v1/live-capture/encounters/{encounterId}/observations` - Critical alert display: if the response includes synchronous alerts, show them prominently with severity, threshold details, and recommended actions 3. Add `POST /api/v1/live-capture/encounters` flow for creating a new encounter with initial vitals. 4. Optimize for tablet: large touch targets, minimal scrolling, landscape layout. 5. Update router default route for `CLINICIAN` → `/live-capture`. **Files:** New `views/LiveCaptureView.vue`, `router/index.ts`, `stores/batches.ts` or new `stores/liveCapture.ts`, `components/AppHeader.vue`. **Dependency:** None. --- ## P4 — EntryForm missing allergy, medication, and discharge fields ### Problem The `EntryForm.vue` component captures patient demographics (name, DOB, sex, blood type, emergency contact), encounter context (admission date, department, room/bed, admission reason), and observations. However, it does not render fields for: - **Allergies** (`allergiesJson`, `noKnownAllergies`) — required for `ALLERGY_UPDATE` batches - **Medications** (`medicationsJson`, `noActiveMedications`) — required for `MEDICATION_LIST` batches - **Discharge diagnosis** — part of the encounter context - **Encounter status** (active/discharged) — affects encounter creation during promotion The backend `DraftService` validates completeness for these batch types and will reject submission with `BATCH_INCOMPLETE`, but the entry clerk has no way to enter the required data. ### Why fix Two of the seven batch types (`ALLERGY_UPDATE` and `MEDICATION_LIST`) cannot be completed through the UI. Entry clerks working on these batch types will be blocked at submission with a validation error they cannot resolve without backend support. ### How to fix 1. Add conditional field sections to `EntryForm.vue` based on `batchType`: - **All types**: patient demographics (existing), encounter context (existing) - **ALLERGY_UPDATE**: allergies list (add/remove items) + `noKnownAllergies` checkbox - **MEDICATION_LIST**: medications JSON editor (add/remove entries) + `noActiveMedications` checkbox - **ENCOUNTER_SUMMARY**: discharge diagnosis text field, encounter status toggle - **MIXED**: all fields visible 2. Update `saveDraftPatient` to include `allergiesJson` and `noKnownAllergies`. 3. Add a medications section that saves to the draft patient's `medicationsJson` field. 4. The verification form should also render these fields for review. **Files:** `components/EntryForm.vue`, `components/VerificationForm.vue`, `types/index.ts`. **Dependency:** None. --- ## P4 — No corrections/supersession UI ### Problem The PRD describes a correction workflow: "A correction creates a new batch with `supersedesBatchId` pointing to the original." The backend fully implements supersession: upload with `supersedesBatchId`, promotion marks original observations as superseded, and `GET /patients/{id}/digitization-history` shows correction chains. The Vue frontend has no UI to: - Create a correction batch (upload with `supersedesBatchId`) - View which batch superseded which - See superseded observations vs active observations in patient history - Navigate the correction chain ### Why fix Corrections are a daily clinical workflow. When a promoted batch has an error (wrong potassium value, incorrect admission date), the only recourse is direct API calls or database access. Without a corrections UI, the audit trail — one of the system's core value propositions — cannot be maintained through normal operator workflow. ### How to fix 1. Add a "Create Correction" button on promoted batch detail views. 2. The button pre-fills `supersedesBatchId` and `patientId` in the upload form. 3. Add a `PatientHistoryView.vue` at `/patients/{id}/history`: - Timeline showing all batches for the patient - Correction chains visualized (original → correction arrows) - Superseded observations shown with strikethrough styling - Active vs superseded observation counts 4. Add batch detail panel showing supersession info when `supersedesBatchId` is set. **Files:** New `views/PatientHistoryView.vue`, `components/IntakeView.vue` (correction button), `router/index.ts`, `stores/batches.ts`. **Dependency:** None. --- ## P4 — No toast notification system or success feedback ### Problem The Vue frontend has no toast/snackbar notification system. All success and error feedback is either: - Inline `errorMessage` refs that render as red text below forms - Implicit (redirect after success with no confirmation) - Console errors (invisible to users) Operations like "batch submitted for verification," "batch verified," "observation saved," and "batch rejected" provide no positive feedback to the user. ### Why fix Clinical data entry is high-stakes work. Entry clerks need immediate confirmation that their saves succeeded, verifiers need confirmation that rejections were sent, and approvers need clear feedback that promotion completed (or was deferred). Silent success breeds anxiety and repeat submissions. ### How to fix 1. Add a toast notification composable (`useToast`) or install `vue-toastification`. 2. Show success toasts for: observation saved, draft auto-saved, batch submitted, batch verified, batch rejected, batch approved. 3. Show error toasts for: API failures, validation errors, auth errors. 4. Show warning toasts for: presigned URL about to expire, batch already assigned. 5. Show info toasts for: promotion deferred (202), session refreshed. 6. Toast position: top-right, auto-dismiss after 4 seconds. **Files:** New `composables/useToast.ts` or install package, update all views/components. **Dependency:** None. --- ## P4 — No frontend tests ### Problem The Vue frontend has zero tests. No unit tests (Vitest), no component tests (Vue Test Utils), and no end-to-end tests (Cypress/Playwright). `package.json` does not include any test dependencies or scripts. ### Why fix The frontend handles critical clinical workflows: data entry, verification, approval. Untested UI components mean regressions in form validation, API calls, auth flow, and role-based routing go undetected until a user encounters them in production. ### How to fix 1. Add Vitest + Vue Test Utils as dev dependencies. 2. Priority unit tests: - Auth store: login flow, token refresh, logout, role-based permissions - Batch store: CRUD operations, error handling, pagination - Router guards: unauthenticated redirect, role-based access 3. Priority component tests: - `EntryForm`: renders fields, saves on blur, validates observations, submits - `VerificationForm`: all-checked enables approve, reject requires reason - `PatientSearch`: debounced search, selection emits event - `ObservationRow`: renders observation codes, value validation 4. Add a `test` script to `package.json`: `"test": "vitest"`. 5. Optionally add Playwright for E2E tests of the full login → entry → verify → approve flow. **Files:** `package.json`, `vitest.config.ts`, new `src/__tests__/` directory. **Dependency:** None. --- # Part F — Observability & Test Coverage --- ## P5 — No audit of field-level draft changes ### Problem The PRD requires "Who changed which draft field (field-level diff in event metadata on save)" as an audit requirement. The current `DraftService` saves draft data (patient, encounter, observations) but does not record which fields changed or the before/after values. The `DigitizationEvent` for `EntryStarted` is written once; subsequent saves produce no events. ### Why fix In a clinical data quality dispute ("the verifier says the temperature was entered as 38.7 but the original chart shows 37.7"), there is no way to determine whether the entry clerk made the error or whether the value was changed after initial entry. Field-level audit trail is essential for clinical accountability. ### How to fix 1. In `DraftService` save methods, load the existing draft before applying updates. 2. Compare each field; build a `fieldsChanged` list with `{ field, oldValue, newValue }`. 3. If any field changed, write a `DigitizationEvent` with `EventType.DraftFieldUpdated` and the field diff in `MetadataJson`. 4. Debounce at the service level: if a `DraftFieldUpdated` event was written for the same batch within the last 30 seconds, update the existing event's metadata rather than creating a new row (prevents audit noise from auto-save). **Files:** `DraftService.cs`, `DigitizationEventType.cs` (add `DraftFieldUpdated`). **Dependency:** None. --- ## P5 — Integration test gaps for deferred promotion and retry ### Problem The test suite has 45 tests across 5 files: `DraftEntryTests` (7), `VerificationTests` (11), `PromotionTests` (10), `CorrectionSupersessionTests` (5), `LiveCaptureIntegrationTests` (12). Key untested scenarios: 1. **Deferred promotion**: no test for the 202 `PROMOTION_DEFERRED` path — when `ApproveAndPromoteAsync` throws an infrastructure exception, the controller should set batch to `APPROVED` and create a `PromotionAttempt`. 2. **Promotion retry**: no test for `PromotionRetryService` picking up deferred batches and retrying. 3. **Retry exhaustion**: no test for the retry limit being reached (batch stuck in `APPROVED` permanently). 4. **Concurrent batch creation**: no test for two simultaneous uploads with the same SHA-256. 5. **Concurrent assignment**: no test for two `PATCH .../assign` calls for the same batch. 6. **Auth flow**: no tests for login, token refresh, logout, or role-based access control. 7. **Work queue ordering**: no test for FIFO ordering of work queue items. ### Why fix The deferred promotion path (P0 issue above) is the most critical untested code path. Without integration tests, the fix for the `PromoteAsync` gap cannot be verified. Concurrent operation tests are needed to validate the Redis-based assignment lock and SHA-256 deduplication under contention. ### How to fix 1. **Deferred promotion tests** (validates P0 fix): - Simulate infrastructure failure during `ApproveAndPromoteAsync` → verify batch is `APPROVED` with `PromotionAttempt` → verify retry creates clinical entities. - Verify exhausted retries leave batch in `APPROVED` with no `NextRetryAt`. 2. **Concurrent operation tests**: - Parallel `PATCH .../assign` for same batch → verify exactly one succeeds. - Parallel upload with same SHA-256 and patient → verify exactly one succeeds. 3. **Auth tests**: - Login with valid/invalid credentials → verify tokens. - Refresh with valid/expired/revoked token → verify behavior. - Access protected endpoint without token → verify 401. - Access role-restricted endpoint with wrong role → verify 403. 4. **Work queue tests**: - Submit 3 batches at different times → verify verification queue returns oldest first. **Files:** New `PromotionRetryTests.cs`, `ConcurrencyTests.cs`, `AuthTests.cs`, `WorkQueueTests.cs` in `VigilCareRecordsAPI.Tests/`. **Dependency:** P0 fix (PromoteAsync). --- ## P5 — MetricsCollectorService does not track APPROVED or retry-pending batches ### Problem `MetricsCollectorService` collects `digitization_batches_by_status` gauge for all 8 statuses and `digitization_queue_age_seconds` for the oldest `PENDING_VERIFICATION` batch. However, it does not report: - Age of the oldest `APPROVED` batch (waiting for promotion retry) - Count of `PromotionAttempt` records with `NextRetryAt` in the past (overdue retries) - Count of exhausted retries (`NextRetryAt IS NULL AND NOT Succeeded`) ### Why fix A batch stuck in `APPROVED` with exhausted retries is invisible in Prometheus dashboards. Operators won't know that a patient's vitals are trapped in limbo unless they query the database directly or check Seq logs. ### How to fix 1. Add gauge: `digitization_promotion_pending_retries` — count of `PromotionAttempt` records where `!Succeeded && NextRetryAt != null`. 2. Add gauge: `digitization_promotion_exhausted_total` — count of batches in `APPROVED` with all `PromotionAttempt` records having `NextRetryAt == null`. 3. Add gauge: `digitization_approval_queue_age_seconds` — age of oldest `APPROVED` batch (mirrors `queue_age_seconds` pattern). 4. Update `MetricsCollectorService.CollectMetricsAsync` to query these metrics. **Files:** `DiagnosticsMetrics.cs`, `MetricsCollectorService.cs`. **Dependency:** None. --- # Summary matrix | # | Issue | Priority | Part | Status | |---|-------|----------|------|--------| | 1 | PromoteAsync missing clinical entities | P0 | A | Done | | 2 | Patient dedup by exact name+DOB | P0 | A | Done | | 3 | Batch assignment inconsistent state | P1 | A | Done | | 4 | Concurrent batch creation race | P1 | A | Done | | 5 | No health check endpoints | P2 | B | Done | | 6 | No CORS configuration | P2 | B | Done | | 7 | Redis failure crashes startup | P2 | B | Done | | 8 | Promotion retry metrics missing | P2 | B | Done | | 9 | JWT key not validated on startup | P3 | C | Done | | 10 | No rate limiting on auth | P3 | C | Done | | 11 | Credentials in plaintext config | P3 | C | Open | | 12 | No document access audit | P3 | C | Done | | 13 | No FluentValidation | P2 | D | Done | | 14 | No user management endpoints | P4 | D | Done | | 15 | No batch cancel/void | P4 | D | Open | | 16 | No sort parameters on lists | P4 | D | Open | | 17 | No clinical approval view | P2 | E | Open | | 18 | No live capture view | P2 | E | Open | | 19 | EntryForm missing allergy/med fields | P4 | E | Open | | 20 | No corrections/supersession UI | P4 | E | Open | | 21 | No toast/notification system | P4 | E | Open | | 22 | No frontend tests | P4 | E | Open | | 23 | No field-level draft audit | P5 | F | Open | | 24 | Integration test gaps | P5 | F | Open | | 25 | MetricsCollector missing retry gauges | P5 | F | Open | --- ## Suggested implementation sequence ```mermaid flowchart TD subgraph correctness [Part A — Correctness] P0A[P0: Unify PromoteAsync] P0B[P0: Patient dedup normalization] P1A[P1: Assignment state transition] P1B[P1: Concurrent batch creation guard] end subgraph infra [Part B — Infrastructure] P2H[P2: Health checks] P2C[P2: CORS configuration] P2R[P2: Redis graceful startup] P2M[P2: Promotion retry metrics] end subgraph security [Part C — Security] P3J[P3: JWT key validation] P3R[P3: Rate limiting] P3S[P3: Secrets management] P3D[P3: Document access audit] end subgraph api [Part D — API] P2V[P2: FluentValidation] P4U[P4: User management] P4X[P4: Batch cancel/void] P4S[P4: Sort parameters] end subgraph vue [Part E — Vue Frontend] P2A[P2: Clinical approval view] P2L[P2: Live capture view] P4E[P4: Entry form fields] P4F[P4: Corrections UI] P4T[P4: Toast notifications] P4FT[P4: Frontend tests] end subgraph obs [Part F — Observability] P5D[P5: Field-level draft audit] P5T[P5: Integration test gaps] P5M[P5: Metrics collector gaps] end P0A --> P5T P2A --> P4E P2L --> P4T P2V --> P4U ``` ### Sprint-sized batches | Batch | Items | Outcome | |-------|-------|---------| | **1 — Correctness** | P0 unify PromoteAsync, P0 patient dedup normalization, P1 assignment state transition, P1 concurrent batch creation guard | Promotion retry creates complete clinical records; no duplicate patients from name variation | | **2 — Infrastructure** | P2 health checks, P2 CORS, P2 Redis graceful startup, P2 promotion retry metrics | Production-deployable infrastructure; orchestration-ready | | **3 — Security** | P3 JWT validation, P3 rate limiting, P3 secrets management, P3 document access audit | Compliance-ready auth; HIPAA audit trail for document access | | **4 — API hardening** | P2 FluentValidation, P4 user management, P4 batch cancel, P4 sort parameters | Admin UI and integration teams unblocked; consistent validation | | **5 — Vue frontend** | P2 clinical approval view, P2 live capture view, P4 entry form fields, P4 corrections UI, P4 toast notifications | All PRD workflows accessible through the UI | | **6 — Observability** | P5 field-level audit, P5 integration tests, P5 metrics collector gaps, P4 frontend tests | Full audit trail; regression safety net for Batch 1 fixes | --- ## Testing strategy (cross-cutting) For each fix, add or extend tests in `VigilCareRecordsAPI.Tests/`: - **Promotion tests** (Batch 1): Deferred promotion → retry → verify clinical entities created; patient name normalization dedup; concurrent assignment race. - **Infrastructure tests** (Batch 2): Health check endpoints return expected status; CORS headers present on cross-origin requests; app starts with Redis down. - **Security tests** (Batch 3): Rate limit on login; JWT with short key rejected at startup; document access event written on batch detail. - **API validation tests** (Batch 4): FluentValidation returns 422 with field-level errors; user CRUD lifecycle; batch cancel transitions. - **Frontend tests** (Batch 5): Vitest component tests for EntryForm, VerificationForm, approval flow; Playwright E2E for full workflow. - **Integration tests** (Batch 6): End-to-end deferred promotion → retry → verify clinical entities; field-level audit on draft save. --- ## Out of scope (unless explicitly requested) - HL7v2 ADT message support (FHIR inbound only in VigilCareClinical) - OCR or automated field extraction (explicitly excluded in PRD v1) - Multi-facility federated identity (single-tenant per deployment in v1) - Full EMR functionality (billing, pharmacy inventory, scheduling) - SMART on FHIR authorization (OAuth2 scopes for EHR launch context) - Offline-first PWA for intermittent connectivity (documented as future extension) - Kubernetes manifests, Helm charts, or CI/CD pipeline definitions - Grafana dashboard provisioning (infrastructure exists but dashboards are manual) --- ## Success criteria When complete, the system should support: **Data Integrity (Part A)** - Deferred promotion retries create complete clinical entities (Patient, Encounter, Observation) identical to first-attempt promotion. - Patient matching normalizes name comparison to prevent duplicates from case/spacing variations. - Batch assignment transitions to `IN_ENTRY` immediately, with no inconsistent interim state. - Concurrent uploads with same SHA-256 for same patient create exactly one batch. **Infrastructure (Part B)** - Health checks report PostgreSQL, Redis, and MinIO status; orchestrators route around failures. - CORS allows the Vue frontend to call the API from any configured origin. - Application starts successfully even when Redis is temporarily unreachable. - Promotion retry attempts are countable via Prometheus metrics. **Security (Part C)** - JWT misconfiguration (short key) fails at startup, not at first request. - Auth endpoints are rate-limited (10 attempts per 5-minute window). - Secrets are managed via environment variables, not plaintext config files. - Document access (presigned URL generation) is logged in the audit trail. **API (Part D)** - All request DTOs have FluentValidation validators with consistent 422 error shapes. - Administrators can create, update, and deactivate users via API. - Erroneously created batches can be cancelled by administrators. - List endpoints support configurable sorting. **Vue Frontend (Part E)** - Clinical approvers have a dedicated approval view with proper promotion flow. - Clinicians have a bedside live capture view for real-time vital sign entry. - Entry clerks can complete allergy, medication, and encounter summary batch types. - Correction batches can be created and tracked through the UI. - All operations provide toast notification feedback. **Observability (Part F)** - Draft field changes are logged with before/after values for clinical accountability. - Integration tests cover deferred promotion, retry, concurrent operations, and auth flow. - MetricsCollector tracks promotion retry status for Prometheus dashboards.