Generated by /office-hours on 2026-05-22 Branch: main Repo: validcare/vcflow Status: APPROVED (2026-05-22, Scott — /office-hours feedback-incorporation session) Mode: Startup (intrapreneurship) Supersedes: scott-main-design-20260521-230737.md
What this revision is. v1 (the file above) produced the strawman + four-fixture mockup. Scott reviewed it on 2026-05-22 and returned 8 feedback points as the final input for the wireframe phase — the wireframes are reworked once against this doc, then spec authoring begins; there is no further wireframe re-review. This v2 carries v1 forward unchanged where v1 was not contested, and reworks the layout, zones, and edge states against the 8 points. The new content is concentrated in §Review Feedback Incorporation, the reworked §Recommended Approach, and §Wireframe Rework Checklist.
Naming note (carried from v1). The handoff calls this the "Participant Dashboard," which is ambiguous — it can read as the participant's own dashboard (ePRO). This surface is the clinician's dashboard about one participant (eCOA). The spec should be titled "Clinician eCOA — Per-Participant View."
Scott's triage instruction: every one of the 8 points gets an explicit "Slice 1 / Slice 2+" call with recorded rationale — no point silently dropped.
| # | Feedback | Call | Where it lands |
|---|---|---|---|
| 1 | Action bar — no duplicate action paths; recommend, don't gate | Slice 1 | Now banner reworked (D1) |
| 2 | ePRO data visible on this screen | Slice 1 (display) / Slice 2+ (affirmation form) | new ePRO zone (D2) |
| 3 | Journey box date semantics | Slice 1 | rail + step-summary date rework |
| 4 | PI sign-off fast summary view | Slice 2+ — different surface | logged to /signing (Slice 3 Task 17) |
| 5 | External lab results | Slice 1 (display + journey attachment) / Slice 2+ (PDF intake) | new Lab Results zone |
| 6 | Out-of-window / override handling | Slice 1 | override affordances (D3) |
| 7 | Pre-submit confirmation | Slice 1 (visit-level) / separate (per-form field) | confirmation pattern |
| 8 | CRA query / clarification flow — in-context | Slice 1 | role-aware raise-query affordance (D2/D4) |
work-vcflow-epro shipping real data.minDays/maxDays offset, defined in
dateRange guards). Slice 1 call: rail rows and the step
summary show the computed window as a calendar date
range with a countdown to window close (the
compliance deadline). No scheduled-date headline — it does not exist and
is not in Slice 1 scope to build. A visit with no window guard shows no
date. Spec dependency: the aggregate API must compute each visit's
window from context.anchors + the dateRange
config (see Dependencies #1)./signing
inbox (Slice 3 Task 17), a different surface. Call:
deferred — recorded as input to /signing, not built here.
Slice 1's per-participant signature zone (Zone 7) still stands and stays
glanceable for when a PI does land on this surface.Design (not implement) Slice 1 of the vcflow clinician eCOA surface: the per-participant view a study coordinator opens to see where one enrolled participant is in their trial journey and what to do next with them. The deliverable is a strawman narrative + an interactive mockup for internal review. Implementation follows in a later slice, gated on mockup approval.
Today this surface (/participants/:id,
apps/staff/src/app/features/participants/participant-detail.component.ts,
~285 lines) is a tabbed flat table of form responses ("Visit Matrix" /
"Timeline" tabs). It makes the coordinator choose a view rather
than see the next task.
Demand for the capability is committed, not speculative: the
eCOA umbrella tracker (work-vcflow-ecoa.md) classifies it
as "Day-1-required clinical capability — the clinician-side
data-collection backbone," targeted for the first commercial study. The
open question is not whether to build a clinician
per-participant surface but what shape it takes. The 2026
industry direction corroborates the chosen shape: clinical dashboards
are moving from static data displays to predictive prioritization —
classify into urgency tiers, clinicians act rather than sort.
Validcare's legacy platform is a two-system split:
The wedge is to collapse the two-system split. vcflow natively owns forms, data, the XState journey engine, and the eCOA workflow in one platform. The dashboard's authoritative, live, journey-driven state is a correctness + trust argument vcpi structurally cannot make.
Primary persona: the study coordinator. Highest-frequency user; minutes per participant, not hours; does visit execution and clinician-completed eCOA data capture across a caseload. The default layout is optimized for the coordinator.
Secondary persona, now first-class on this surface: the monitor / CRA. v1 treated the surface as coordinator-only. Point 8 pulls the CRA in — not as a separate screen but as a role-scoped view of the same surface: the monitor gains a raise-query affordance and loses the coordinator's data-capture buttons (see Zone 3 and Open Question 8). PI/investigator e-signature remains secondary — surfaced, not centered.
Narrowest wedge: one participant, one screen, one glanceable next action — a journey-engine-driven view that ranks rather than lists.
/casebook),
not the participant's own ePRO app.libs/shared-ui primitives
is a separate later task, out of scope here./casebook. Not the ePRO participant
app. (Point 2 surfaces ePRO data here; it does not pull in the
ePRO app.)/participants/:id. Existing
behavior is carried forward or deliberately replaced, never
silently dropped.libs/shared-ui primitives when they genuinely suit;
build net-new when they don't.The rail separates two things v1 had fused: journey position (the single moving frontier — the state machine) and per-visit data completion (tracked independently). That split is what lets a missed visit's data be collected late without two concurrent journey states.
meta.visitRef (the engine models one form per state; a
multi-form visit is a chain of states — see §Journey-config-driven
behavior). The rail tiers operate on visits; the XState frontier is a
single state within the Now visit.The journey config (study.json + the XState machine) is
the single source of truth for override and sequencing
— the dashboard reads and renders, it does not bake journey logic into
the UI. OQ5 was investigated against the real engine
(machine.factory.ts, guard.registry.ts,
types.ts); the resolved model:
meta.visitRef group.
The XState machine models one form per state; a
multi-form visit is a chain of states sharing a
meta.visitRef ("visit-1", "visit-2"). The dashboard groups
states by visitRef to render a visit and its forms. No
engine change needed — visitRef already exists.soft today, hard
is net-new. Today every window is soft: a
dateRange guard failure fires a
recordWindowDeviation action and the transition still
proceeds. That is the soft-window behavior — Slice 1 ships
against it with zero engine change. A hard window
(block the transition once the window closes) does not exist; it is a
small net-new per-state flag,
windowPolicy: 'hard' | 'soft', default
'soft'.FormResponse saves independently; the journey's
FORM_COMPLETED chain catches up. So an explicit "unordered
set" config, if the engine ever gains one, changes nothing here — the
dashboard already never gated.The only hard gates on this surface are data integrity (an open query blocks visit signature) and hard-window non-collectability. Everything else the banner recommends.
Data completion is already separate from journey
state — confirmed in the engine: FormResponse rows
persist on form save, independent of the XState machine; a form for a
past visit can be filled after the frontier has advanced, and nothing
blocks it. D3's core bet (collect late data without two concurrent
journey states) needs no new separation — it is how the
platform already works. The dashboard computes a visit's completeness
from its FormResponse rows (all forms in the
visitRef group present), not from journey state — that
computation drives the Done-tier "Data outstanding → Complete" flip.
From the v1 session (not re-run for this feedback-incorporation pass — the v2 decisions are feedback-driven, not premise-driven). Two independent cold reads validated the journey-engine wedge and the two-pane structure. Key v1 contributions still load-bearing here:
vc-journey-timeline is a chronological audit component,
the wrong base), that query-list is too heavy for in-step
open items, and that pi-sig-banner composes but must be
wrapped for coordinator-first copy. All still hold.Approach B was selected by Scott in v1 for its long-term scaling to Slice 2+ panels. This revision does not reopen the approach choice — the 8 points are all within Approach B.
A persistent participant identity header spans the
top (study ID, arm, site, enrollment state — no PHI beyond study ID).
Below it, two panes in a fixed 304px 1fr grid.
ParticipantJourneyRail)Three tiers per the Tier-derivation rule. Date semantics (point 3, OQ4 resolved): there is no scheduled-appointment date in vcflow — each rail row shows the computed visit window as a calendar date range and a countdown to window close. A visit with no window guard shows just its name.
All zones are step-scoped — they render the rail-selected step (default: the Now step). There are eight zones, numbered 1–8. v2 inserts Zone 6 (Lab Results) and renumbers the v1 Signature and Audit zones to 7 and 8; v1's zone order is otherwise preserved.
Zone 1 — Now recommendation banner (reworked, D1). The banner is a pointer, not an executor. It carries the "Next action" tag, a plain-language headline, and a one-line "why now." It has no primary button of its own. Its mechanic is fixed: the banner draws a static highlight border on the zone that owns the recommended action, and clicking the banner scrolls that zone into view. There is no scroll-on-load — the banner sits at the top and the owning zone is already near it.
Zone 2 — Step summary. Visit/step name, window range (computed from the anchor + offset), window status, one-line plain-language summary.
Zone 3 — Open Items. Queries + edit-check failures + signature blockers for the selected step, must-resolve-first order. Compact list (net-new; reuses query types/badges only). Quiet empty state when clear.
Zone 4 — ePRO since last visit (new, D2 / point 2).
A read-only summary of participant-reported ePRO entries (e.g. "3 ePRO
responses since last visit · last May 18 · Review"). "Review" opens the
ePRO detail. Collapses to a one-line summary; quiet empty state when the
participant has entered nothing. When a Done step is
selected, the zone shows the ePRO entries that fell within
that visit's window — the "since last visit" framing is
relative to the current visit only; for a Soon step the zone is empty.
The Part 11 clinician-affirmation form ("I reviewed this
participant's ePRO data") is Slice 2+ — designed later, gated
on work-vcflow-epro shipping real data. The zone is laid
out now so ePRO is a planned surface, not a later bolt-on.
Zone 5 — Forms for this visit. Compact rows, status
dot, one button per row — Start / Continue / View. This
is the single executor for form actions (D1); the
recommended row is highlighted by Zone 1 but is otherwise an
ordinary row. The visit's forms (the visitRef group's
states) render in the engine's chain order; all rows stay
actionable regardless of order (recommend, don't gate). When a
Done-tier "Data outstanding" visit is selected, its form rows stay
actionable so the late data can be collected. Monitor role: each row
carries the Zone-3 "Raise query" affordance and the Start / Continue
button is hidden (see the Zone 3 design boundary).
Zone 6 — Lab Results (new, point 5). External lab results attached to this visit / the participant journey. Display only in Slice 1: result rows, source label, collected date, and a link to the source document (e.g. the lab PDF). When a Done step is selected, shows that visit's labs; empty for Soon steps. The display + journey-attachment model is Slice 1; the PDF upload/intake mechanism is Slice 2+ (the umbrella's interim path before Lab Connect). Quiet empty state when no external labs exist for the visit.
Zone 7 — Investigator signature. Wrapped
pi-sig-banner; renders not-ready / pending-PI states with
coordinator-first copy. Routing a completed visit to signature triggers
the pre-submit confirmation (point 7) — see below.
Zone 8 — Audit trail. Step-scoped who-did-what-when, collapsed by default, present on the page (UX principle 5). "View full participant history" link opens the complete participant-wide audit (a separate view).
v1's "Coordinator job list" table (the 8-task → zone mapping) is superseded by the per-zone descriptions above and §Success Criteria's "every panel maps to a task" assertion — deliberately folded in, not silently dropped (P2).
A missed or overdue visit never blocks the journey — the next visit opens on its own window schedule; the dashboard does not gate it on the prior visit being complete. The prior visit drops into the Done tier, and what the clinician can do with it is dictated by the journey config's window policy:
A single consistent confirmation pattern applied to the three dashboard transitions that each write an immutable audit or deviation event — the confirmation is a genuine last check before a permanent write, not a courtesy, and its copy says so. Each shows a summary of what is about to happen and a confirm button — and for the two deviation actions it captures a structured reason (D8):
Per-form, field-level confirm-before-submit is not in this pattern — it belongs to the SurveyJS forms renderer and is separate work.
| Decision | Resolution |
|---|---|
| Information hierarchy | Identity header → Now banner → step detail; rail is persistent orientation. |
| Layout pattern | Two-pane: journey rail + work surface, fixed
304px 1fr. |
| "Next action" surfacing | Now banner = a pointer that highlights the owning zone (D1). |
| Visit + step rendering | Tiered Now / Soon / Done in the rail; a visit = a
meta.visitRef group of states; a visit's forms render
co-equal and all actionable (engine chains them; dashboard does not
gate). |
| Lock semantics | No hard task-order gates — sequencing is recommended, not locked (point 1). Soon steps dimmed + hollow dot. The only hard gates: data-integrity (query blocks signature) and hard-window non-collectability. No vcpi-style "Requires:" tags. |
| Sign workflow | Signature zone (Zone 7); route-to-signature triggers pre-submit
confirmation; cross-participant PI summary is /signing, not
here (point 4). |
| Participant selector | Match /casebook's drill-down — a
← Casebook back link. |
| Empty / first-visit | First-visit fixture: rail Now = first visit, Done · 0, banner points to the first form. |
| Density | Desktop-first, generous whitespace, ~1180px content width. Tablet deferred to the spec. |
| State | Treatment | In mockup? |
|---|---|---|
| Out of window / overdue | Now visit past its window; amber "Overdue" on rail row + step summary + Now banner. A soft-window visit stays collectable. | Yes — fixture 4. |
| Data outstanding (collected late) | A soft-window visit the frontier has passed, still collectable; Done tier, amber "Data outstanding," forms actionable; collecting opens deviation-reason capture (D8). | New — fixture 5. |
| Hard-window missed | A hard-window visit whose window closed with no data; auto-flagged "Missed" in Done, read-only; clinician records the deviation reason. | New — fixture 5 (paired). |
| Query-blocked | Forms complete but an open query blocks completion/signature; Zone 7 stays "not ready." | Partially (fixture 2). Pure "forms done, query blocks" deferred to spec. |
| CRA query raise | Monitor-role view; coordinator action buttons hidden; "Raise query" affordance on Forms rows + Open Items. | New — fixture 6. |
| ePRO present | Zone 4 shows the ePRO summary; "Review" opens detail. | New — show populated in fixtures 2/3. |
| External lab attached | Zone 6 shows lab result rows + source-document link. | New — show populated where a visit has labs. |
| Withdrawn / discontinued | Journey ended early; surface read-only; banner = neutral status. | Designed; deferred from the mockup. |
| Unclassifiable journey state | Fallback: render in Now, neutral "Review participant status," surface raw state. | Deferred to spec. |
The single concrete punch list for the wireframe-rework session. Rework once against this, then author the spec.
Final fixture roster (v1 had 4; v2 reworks all 4 and adds 2): (1) newly-enrolled · (2) visit-in-progress · (3) awaiting-PI-signature · (4) visit-overdue · (5) out-of-window handling — a soft-window "Data outstanding" visit in the Done tier (collect-late + deviation-reason capture) and a hard-window "Missed" visit · (6) monitor-role view (CRA raise-query). Fixtures 5 and 6 may be interactive states layered on an existing fixture rather than standalone frames, at the rework session's discretion — but both must be walkable.
dateRange guards: an anchor +
minDays/maxDays); anchors (enrollment,
firstTreatment) are per-participant ISO dates in
context.anchors; windows are computed at runtime in guards
and are not exposed by any API today. Resolution: point
3 shows the computed window range + countdown-to-close, no
scheduled-date headline. The aggregate API must compute the window per
visit (Dependencies #1). Key file refs:
apps/api/src/modules/journey/registry/types.ts:33-47
(JourneyContext.anchors), types.ts:59-65
(DateRangeGuardConfig),
apps/api/src/modules/journey/registry/guard.registry.ts:107-143
(the dateRange window computation),
studies/PILOT-001/study.json:191-208 (example
dateRange guards — isInV2Window,
isInV3Window),
apps/api/src/modules/forms/entities/form-response.entity.ts:47
(visitRef column — string only, no date field).machine.factory.ts,
guard.registry.ts, types.ts. Findings: (a)
window policy — soft is exactly today's
behavior (a dateRange guard failure records a deviation and
the transition proceeds); hard is a small net-new per-state
flag windowPolicy: 'hard' | 'soft' (default
'soft'). (b) next-step structure — the
engine models one form per state, chains forms sequentially, and groups
them into a visit by meta.visitRef; there is no
unordered-set construct, and the dashboard does not need one
(order-agnostic by design — see §Journey-config-driven behavior). (c)
data-completion separation — already real:
FormResponse rows persist independently of the XState
machine; a past visit's form can be filled after the frontier advances.
Resolution: the only net-new config is the
windowPolicy flag; everything else D3 needs already exists.
Key file refs:
apps/api/src/modules/journey/registry/machine.factory.ts:88-149
(createJourneyMachine),
machine.factory.ts:42-75 (transformStates),
machine.factory.ts:22-32 + :62-65
(TransitionConfig + array-transitions for guarded
branching),
apps/api/src/modules/journey/registry/types.ts:162-178
(StateMeta / StateDefinition — no
windowPolicy today; this is where the field gets added),
apps/api/src/modules/journey/registry/guard.registry.ts:107-143
+ :149 (dateRange + formCompleted
guards),
apps/api/src/modules/journey/entities/journey-state.entity.ts:40-44
(currentState + snapshot),
apps/api/src/modules/journey/activities/journey.activities.ts:69
(getPersistedSnapshot),
apps/api/src/modules/forms/entities/form-response.entity.ts:16-103
(FormResponse — independent of journey),
apps/api/src/modules/forms/services/form-journey-bridge.service.ts:87-93
(onFormCompleted → signal journey),
apps/api/src/modules/journey/services/participant-timeline.service.ts:108-120
(form responses + state history merged separately in the timeline),
studies/EXAMPLE-001/study.json:76-135 and
studies/PILOT-001/study.json (visit/state definitions,
meta.visitRef grouping, transition arrays with
recordWindowDeviation fallthrough).docs/superpowers/specs/2026-05-2X-clinician-ecoa-per-participant-view-design.md
(implementation_project: vcflow), then a plan, then
implementation targeting Demo #2 (2026-06-06).Not applicable — the deliverable is a route inside the
already-deployed apps/staff Angular app (Cloudflare Pages),
covered by the existing CD pipeline.
Implementation prerequisites, ordered by magnitude — spec/plan inputs, not Slice 1 design work:
meta.visitRef; (b) compute each visit's
calendar window from context.anchors + the visit's
dateRange config (today that config lives inside guard
definitions and the visit→window mapping is implicit — the API needs an
explicit mapping); (c) compute each visit's
completeness from its FormResponse rows (drives the
Done-tier "Data outstanding → Complete" flip). Starting points:
apps/api/src/modules/journey/services/participant-timeline.service.ts:48-196
(current timeline shape — incomplete for the aggregate);
apps/staff/src/app/features/participants/participant-detail.component.ts:260-275
(current dashboard fetch — two endpoints today).formResponseId
(participant-detail.component.ts:282); a freshly-enrolled
participant has none. First-form creation is net-new client + backend
work.windowPolicy flag + deviation
recording. Reduced after OQ5: the only net-new journey-config
is a per-state
windowPolicy: 'hard' | 'soft' flag
(default 'soft' = current behavior), plus
hard-window enforcement (block the transition when a
hard window has closed). Recording a protocol deviation + structured
reason on out-of-window collection or a missed visit is also net-new;
the deviation-reason picklist taxonomy needs a source
(protocol-defined or a standard deviation-reason list). Per-visit
data-completion tracking is not net-new —
FormResponse already persists independently of the journey
(OQ5c). Starting points:
apps/api/src/modules/journey/registry/guard.registry.ts:107-143
(current soft-window behavior — the place to extend with hard-window
enforcement); studies/PILOT-001/study.json:191-208 +
transition arrays (the recordWindowDeviation fallthrough
pattern — the place to wire the windowPolicy: 'hard'
branch);
apps/api/src/modules/journey/registry/types.ts:162-178
(StateMeta / StateDefinition — where the
windowPolicy field gets added).work-vcflow-epro exposing queryable participant ePRO
entries. If ePRO data does not exist when Slice 1 ships, Zone 4 renders
its empty state.2026-05-06-queries-and-soft-checks); the
net-new work is the in-context raise affordance + prefilled composer,
and (Open Question 6) a forms monitor-review mode for value-level
queries.ParticipantJourneyRail, a compact open-items list, an
ePRO-summary zone, a lab-results zone, a coordinator-first
pi-sig-banner wrapper, and the confirmation-dialog
pattern.Adjacent surfaces that must not collide: /casebook
(visit-matrix.component), /signing +
/signing/:id (point 4 feeds this),
/dashboard.
The eventual spec must be authored on a feature branch (per
~/code/ branching policy).
Tools panel, Reports panel, Unscheduled-Visit catch-all, per-test
status grid — all Slice 2+. Cross-participant triage stays in
/casebook. The cross-participant PI sign-off summary is
/signing (point 4). The ePRO clinician-affirmation form,
the lab-PDF intake mechanism, and per-form field-level pre-submit
confirmation are all Slice 2+ / separate work. The deviation
exclusion-review surface — where the PI / medical monitor /
sponsor judges whether a recorded deviation is exclusionary — is a
separate downstream surface, not this dashboard.
libs/shared-ui polish is a separate task.
Take the reworked mockup to the wireframe-rework checkpoint and walk it against the §Wireframe Rework Checklist — confirm each of the 8 points landed where this doc says. The state-to-state shape change (newly-enrolled → in-progress → awaiting-signature → overdue → out-of-window handling → monitor-role) is what sells the journey-engine wedge; a static frame does not. On approval, the next concrete action is authoring the Rule-A spec on a feature branch.