Clinician Workspace — How it works
Overview
The providerportal Maven module is the internal clinician workspace — a read-aggregation BFF that
fans out to platform engines over Feign and never imports core. Routes live under
/api/v1/workspace/** (ApiV1Paths.WORKSPACE). Design: MILESTONE16_PLAN.md, CORE_PLAN.md §15.4
(provider-facing).
workforce is the structural reference for domain-module skeletons (Feign propagation, module
descriptor, ArchUnit boundary). The workspace differs in one non-negotiable way: every outbound call
carries the clinician's delegated token — never a service account.
M16-001 delivers the skeleton only. What exists today:
| Ticket | Delivers | Status |
|---|---|---|
| M16-001 | Maven module, /api/v1/workspace namespace, status endpoint, Feign header propagation, ArchUnit fence | done |
| M16-002 | My patients / panel (GET /my-patients, paged, scope-filtered) | done |
| M16-003 | Clinical inbox (results to sign, review tasks) | done |
| M16-004 | Unified worklists | done |
| M16-005 | Unified patient chart summary | done |
| M16-006 | Workspace preferences + saved views | done |
| M16-007 | Milestone gate + MILESTONE16.md | done |
Frontend counterpart: FE-326 (workspace shell), FE-327 (my-patients panel), FE-328
(inbox), FE-329 (worklist), FE-330 (chart), FE-331 (preferences), FE-332
(Playwright gate). Route keys workspace.home, workspace.panel, workspace.inbox,
workspace.worklist, workspace.chart, and workspace.preferences are registered in
AuthorizationCatalog.
Module boundary
- Package:
com.zhenus.uhp.api.providerportal - Dependencies:
common+exchangeonly — ArchUnit fails on anycoreimport - Platform access: Feign clients in
exchange.client.*only; callerAuthorizationand scope headers forwarded byProviderPortalFeignContextPropagationConfig(no service-identity fallback) - Schema:
providerportal(Liquibase masterproviderportal.db.changelog-master.yaml). M16-006 addsworkspace_preferenceandsaved_view; all clinical aggregation remains Feign-sourced.
Permissions
ProviderPortalModuleDescriptor registers workspace.* codes at startup:
| Code | Used by |
|---|---|
workspace.status.read | GET /api/v1/workspace/status |
workspace.panel.read | M16-002 panel |
workspace.inbox.read / workspace.inbox.write | M16-003 inbox actions |
workspace.worklist.read / workspace.worklist.write | M16-004 worklist actions |
workspace.chart.read | M16-005 chart summary |
workspace.preferences.read / workspace.preferences.write | M16-006 preferences |
Status endpoint
GET /api/v1/workspace/status returns module identity (moduleKey, displayName, version) and an
empty capabilities list until aggregation surfaces ship. Gated by workspace.status.read.
My patients panel (M16-002)
GET /api/v1/workspace/my-patients returns a derived, paginated panel of patients the signed-in
clinician may act on. Membership is computed from provider–facility assignments (M3-008), effective
access scope, and recent encounters — never stored locally. Gated by workspace.panel.read.
Query parameters: standard Spring page / size / sort, plus publicDisplay=true to redact patient
names server-side (shoulder-surfing / public-board mode).
Response (PanelPageDto): showPatientNames, assignedFacilityIds (scope indication), and a paged
content list of PanelEntryDto rows (patientId, last encounter metadata, optional display name).
Feign additions: ProviderClient.listFacilityAssignments(providerId) for assignment lookup. Under
ID-001 the provider id equals the caller's person id, so no separate provider-by-person lookup is required.
Clinical inbox (M16-003)
GET /api/v1/workspace/inbox merges the signed-in clinician's pending result reviews (M13-010) with
assigned workflow tasks (M8 / M10-010 searchTasks), sorted abnormal-first then oldest-first. Gated by
workspace.inbox.read.
When a Feign source fails, the response includes unavailableSources (RESULT_REVIEW and/or
WORKFLOW_TASK) instead of silently dropping rows.
Action endpoints delegate to owning engines — the workspace never stores sign-offs locally:
| Workspace route | Delegates to |
|---|---|
POST /inbox/result-reviews/{id}/acknowledge | POST /clinical/result-reviews/{id}/acknowledge |
POST /inbox/workflow-tasks/{id}/complete | POST /workflow/workflow-tasks/{id}/complete |
Write actions require workspace.inbox.write.
Feign additions: ResultReviewClient (list + acknowledge), WorkflowClient.searchTasks (cross-instance task
board query returning PageDto<WorkflowTaskDto>).
Unified worklist (M16-004)
GET /api/v1/workspace/worklists merges queue items assigned to the caller, assigned workflow
tasks, and open orders placed by the caller (PLACED / IN_PROGRESS). Queue rows preserve the
queue engine's service order; workflow and order blocks follow their own timestamps. Gated by
workspace.worklist.read.
Delegate write routes (require workspace.worklist.write):
| Workspace route | Delegates to |
|---|---|
POST /worklists/queue-items/{id}/status?status= | PUT /queue/queue-items/{id}/status |
POST /worklists/workflow-tasks/{id}/complete | POST /workflow/workflow-tasks/{id}/complete |
Feign addition: ClinicalOrderClient.searchOrders for open-order aggregation.
Patient chart summary (M16-005)
GET /api/v1/workspace/patients/{patientId}/chart assembles a one-call chart for an in-scope patient.
ChartAccessGuard runs a single platform access decision and writes a PHI read audit row before any
Feign fan-out; out-of-scope patients receive 404.
Six sections fan out in parallel with a 3-second per-section budget. Slow or failed sources appear in
unavailableSections (CLINICAL_SUMMARY, SUMMARY_PANEL, ACTIVE_MEDICATIONS, OPEN_ORDERS,
REFERRALS, APPOINTMENTS) — never as empty clinical data.
Allergy state is surfaced distinctly via allergyStatus (RECORDED / NONE_KNOWN / NOT_ASKED) from the
summary panel when available.
Feign additions: ClinicalClient.getPatientSummaryPanel, ReferralClient.historyForPatient,
SchedulingClient.findAppointments (optional patientId filter), existing medication and order clients.
Workspace preferences and saved views (M16-006)
Per-user layout keys and saved filter criteria live in the providerportal schema (workspace_preference,
saved_view). Rows are keyed on the resolved user account plus tenant and facility from the session.
Saved views store criteria JSON only, never patient identifiers or result rows.
| Route | Permission |
|---|---|
GET /api/v1/workspace/preferences | workspace.preferences.read |
PUT /api/v1/workspace/preferences/{key}?preferenceValue= | workspace.preferences.write |
GET /api/v1/workspace/saved-views | workspace.preferences.read |
POST /api/v1/workspace/saved-views | workspace.preferences.write |
PUT /api/v1/workspace/saved-views/{id} | workspace.preferences.write |
DELETE /api/v1/workspace/saved-views/{id} | workspace.preferences.write |
Route key workspace.preferences is registered in AuthorizationCatalog (FE-331 backend half).
Caller resolution
CurrentClinicianResolver resolves the signed-in clinician's personId from the JWT personId claim or
a local principal — the same pattern as workforce's CurrentStaffResolver, without importing core
security types.
Standalone run
WORKSPACE_APP_PORT=8093 ./mvnw -pl providerportal spring-boot:run
When bundled in platform-app, the module is component-scanned alongside core; its standalone
ProviderPortalApplication launcher is excluded from the platform scan (same pattern as workforce).
Verification
./mvnw -pl providerportal -am test -Dtest=ProviderPortal* -Dsurefire.failIfNoSpecifiedTests=false
Gate evidence: core/MILESTONE16.md and Milestone16ClinicianWorkspaceIntegrationTest.
Configuration & feature flags
| Setting | Default | Purpose |
|---|---|---|
WORKSPACE_APP_PORT | 8093 | Standalone listen port when run as its own Spring Boot app |
WORKSPACE_SERVICE_URL | ${UHP_PLATFORM_URL} | Feign loopback target after a future split |
Chart section fan-out uses a 3-second per-section timeout (hard-coded in
ChartSummaryServiceImpl); slow Feign sources surface in unavailableSections, not as empty
clinical data.
Related features
- Clinical — observations, orders, result reviews the inbox and chart aggregate
- Workflow — assigned tasks in inbox and worklist
- Queue — assigned queue items in the worklist
- Workforce — structural reference for domain-module skeleton; staff portal is separate from this clinician BFF
- Lab / Imaging — fulfiller modules whose results appear in inbox/chart
Why it is this way
Read-aggregation BFF, not a clinical store. Every clinical fact stays in the owning engine;
providerportal only merges views and stores per-user layout. Writes on inbox/worklist routes
delegate to clinical/workflow/queue APIs in the same request — the workspace never caches sign-offs
or status locally.
The caller's token on every Feign hop. Unlike scheduled jobs that need SEC-018 service identity,
the workspace always forwards the clinician's delegated Authorization and scope headers. A service
account would bypass patient-access decisions the panel and chart rely on.
Access before chart fan-out (SEC-011 posture). ChartAccessGuard decides platform access and
writes a PHI audit row before any Feign call. Out-of-scope patients answer 404, not 403, so the
chart route does not become an existence oracle.
Saved views are criteria-only. Filter JSON must never carry patient ids or result rows — those belong in live queries against scoped engines, not in a reusable view another session could replay.
Traps
⚠ unavailableSources / unavailableSections mean upstream failure, not "empty". Inbox,
worklist, and chart responses distinguish a Feign outage from legitimately zero rows. UI that treats
a missing block as "nothing to do" hides partial platform outages.
⚠ Saved-view criteria reject patientId / personId keys (case-insensitive) — storing patient
identifiers in criteria would let one clinician's filter leak another patient's scope when replayed.
⚠ JaCoCo gate on providerportal is ≥ 0.80 — preference and controller paths need explicit
tests; bundle coverage failures blocked M16-006 CI until expanded.
⚠ Do not import core. ArchUnit fails on any com.zhenus.uhp.api.core dependency; reach platform
engines only through exchange.client.* Feign interfaces.
⚠ Panel membership is derived, never stored. GET /my-patients recomputes from assignments and
encounters each call — caching panel rows in providerportal would drift from access-scope changes.