Skip to content

Client/Server: Initial session interactions - #574

Open
Zaid-Ajaj wants to merge 7 commits into
masterfrom
zaid/initial-session-interactions
Open

Client/Server: Initial session interactions#574
Zaid-Ajaj wants to merge 7 commits into
masterfrom
zaid/initial-session-interactions

Conversation

@Zaid-Ajaj

Copy link
Copy Markdown
Collaborator

Description

This PR contains the initial session interactions implementation between client and server. It scaffolds a skeleton for the launch sequence of GenPRES from an external application via the /session?launch={token}. Received on the client initially, it is transferred to the server from which the redeem token is derived and sent back to the client. Afterwards, the client uses the redeem token to acquire session information.

The idea is that subsequent operations on the client will carry a redeem token to validate who can do what and when.

The actual derivation, name and validation of the redeem token is stubbed out for now. As well as the functionality to retrieve the patient information.

When there is no launch token, the session is anonymous and continues to work the way it used to be; being able to parse the patient info still from the initial URL.

Finally, this PR fixes an issue in Feliz.Router where it never triggered UrlChanged because the dispatch function it acquires was not stable. This is now resolved (with a bit of help from Claude)

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
@Zaid-Ajaj
Zaid-Ajaj requested a review from halcwb September 7, 2026 16:02
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Test Results

1 554 tests  ±0   1 548 ✅ ±0   2m 25s ⏱️ +35s
   16 suites ±0       6 💤 ±0 
   16 files   ±0       0 ❌ ±0 

Results for commit b970395. ± Comparison against base commit 0557a24.

This pull request removes 16 and adds 16 tests. Note that renamed tests count towards both.
GenCore.EnteralAccess ‑ 0 can create enteral access 
GenCore.EnteralAccess ‑ 1 can create enteral access duodenal
GenCore.EnteralAccess ‑ 2 can create enteral access oral
GenCore.EnteralAccess ‑ 3 can create enteral access oral
GenCore.EnteralAccess ‑ 4 can create enteral access anyenteral
GenCore.EnteralAccess ‑ 6 can create enteral access 
GenCore.EnteralAccess ‑ 7 can create enteral access anyenteral
GenCore.EnteralAccess ‑ 8 can create enteral access gastric
GenCore.EnteralAccess ‑ 9 can create enteral access 
GenCore.venousAccess ‑ 0 can create venous access central
…
GenCore.EnteralAccess ‑ 0 can create enteral access duodenal
GenCore.EnteralAccess ‑ 1 can create enteral access anyenteral
GenCore.EnteralAccess ‑ 2 can create enteral access duodenal
GenCore.EnteralAccess ‑ 3 can create enteral access anyenteral
GenCore.EnteralAccess ‑ 4 can create enteral access gastric
GenCore.EnteralAccess ‑ 6 can create enteral access gastric
GenCore.EnteralAccess ‑ 7 can create enteral access duodenal
GenCore.EnteralAccess ‑ 8 can create enteral access oral
GenCore.EnteralAccess ‑ 9 can create enteral access anyenteral
GenCore.venousAccess ‑ 0 can create venous access anyvenous
…

♻️ This comment has been updated with latest results.

@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

The PR does not yet appear safe to merge because delayed launch or redemption completions can still replace a newer active session.

Summary

  • Adds shared session token and content contracts plus server launch/redeem endpoints.
  • Adds client session parsing, asynchronous launch/redemption handling, and session identity display.
  • Introduces a patient-loading port and stub adapter for redeemed sessions.
  • Keeps anonymous patient URL parsing and dependent patient-state resets.
  • Uses a ref-backed router callback so navigation listeners invoke the latest callback.

Diagram

sequenceDiagram
    participant E as External application
    participant C as GenPRES client
    participant S as GenPRES server
    participant P as Patient port

    E->>C: "Open /session?launch={token}"
    C->>S: launchSession(launch token)
    S-->>C: redeem token
    C->>C: "Navigate to /session?redeem={token}"
    C->>S: redeemSession(redeem token)
    S->>P: loadPatientData(patient ID)
    P-->>S: Patient
    S-->>C: SessionContent
    C->>C: Display user and patient context
Loading

Comment on lines +10 to +20
let launchSession (env: AppEnv) (token: SessionLaunchToken) : Async<Result<SessionRedeemToken, string>> =
// TODO: implement proper session launch logic,
// which checks the validity of the token and returns a redeem token if valid, or an error if invalid.
let (SessionLaunchToken token) = token
async { return Ok(SessionRedeemToken "test-redeem-token") }

let redeemSession (env: AppEnv) (token: SessionRedeemToken) : Async<Result<SessionContent, string>> =
async {
let (SessionRedeemToken token) = token

if token = "demo-error" then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Session Tokens Are Unvalidated

These remotely exposed operations accept effectively arbitrary tokens. launchSession ignores the supplied launch token and always returns test-redeem-token, while redeemSession accepts every token except demo-error and returns session identity and patient content. Because the remoting API has no separate authentication gate, any remote caller can create and redeem a session without proving identity or permissions. These endpoints must fail closed until redeem tokens are validated, bound to a session, and protected against replay.

How this was verified: The public remoting API routes caller-supplied tokens directly to functions that issue a fixed token and return session content without an authentication check.

Knowledge Base Used:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a stub, not the final production code. The real production code will follow: docs/scenarios/integration/GenPRES-MainEHR-Integration-V8.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Since this is explicitly a scaffold and the token derivation/validation is deferred to docs/scenarios/integration/GenPRES-MainEHR-Integration-V8.md, I would treat this as a release-risk reminder rather than a defect in the intended production flow. The key constraint is that these stubs must not be exposed as production authentication: until the documented implementation lands, the endpoints should remain clearly non-production or fail closed.

Comment on lines +1182 to +1188
let state =
{ state with
SessionContext = SessionContext.Content(Resolved content)
Patient = Some content.Patient
}

state, Cmd.none

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Redeeming Leaves Stale Results

A successful redemption assigns content.Patient directly instead of using the existing UpdatePatient lifecycle. When the redeemed patient differs from the patient currently in state, the order context, order plan, nutrition plan, formulary, and parenteralia remain associated with the previous patient because none of them are reset or reloaded. This can display clinically stale results beside the newly redeemed patient.

Knowledge Base Used:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be part of a different pathway. The existing pathway only is meant for anonymous use and will be separate from the pathway this applies to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That distinction addresses the concern. If the redeemed-session flow always starts without the anonymous patient/order state, assigning content.Patient directly cannot leave anonymous results associated with it. The stale-state risk would only apply if a session could be redeemed after an existing patient had already been loaded in the same app instance. No change is needed for the separate pathways described here.

Comment on lines +1151 to +1152
| SessionContextMsg(SessionContextMsg.Launched(SessionRedeemToken token)) ->
state, Cmd.navigate ("session", [ "redeem", token ])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale Session Requests Win

Launch and redemption completions are not tied to the session request that is still current. If the user changes the URL or starts another session while an earlier request is delayed or awaiting the server, the older Launched result still navigates to its redeem URL and the older Redeemed result still overwrites the active session and patient. Carry the initiating token or another request identity through completion and ignore results that no longer match the current session.

Knowledge Base Used: Prescribing client workflows

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Work in progress but good to note. Now just a stub and not real working code.

Comment on lines +756 to +762
Cmd.batch
[
Cmd.ofMsg (UpdatePatient pat)
match sessionInfo with
| SessionContext.Launching token -> Cmd.ofMsg (SessionContextMsg(SessionContextMsg.Launch token))
| SessionContext.Redeeming token -> Cmd.ofMsg (SessionContextMsg(SessionContextMsg.Redeem token))
| _ -> Cmd.none

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Anonymous Routes Retain Identity

An anonymous URL change does not reset state.SessionContext. After leaving a redeemed session, the title bar can therefore continue showing the previous user's name even though the new route was parsed as anonymous. Reset the session context to SessionContext.Anonymous during this transition so the displayed identity matches the active route.

Knowledge Base Used: Prescribing client workflows

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zaid-Ajaj this is something that needs to fixed, I think.

@halcwb halcwb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment on the problem of session context still showing user if switching to anonymous should work in this PR.

@halcwb

halcwb commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@Zaid-Ajaj I think we should first agree on the implementation plan and then fix this pr. @7sharp9 if you can also have a look? Thanks!

| Interactions
| Settings

[<RequireQualifiedAccess>]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SessionContext is doing two jobs. parseSessionInfo : string list -> SessionContext can only ever return Anonymous | Launching | Redeeming, but the type also carries Error of string and Content of Deferred<SessionContent>.

This could benefit from splitting the responsibilities


| UrlChanged sl ->
let pat, page, lang, discl, med = sl |> parseUrl
| UrlChanged newUrl ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UrlChanged overwrites Patient unconditionally with parsePatient newUrl.

After a redeem sets Patient = Some content.Patient, the next navigation to any non-patient route parses None and wipes it.

Shouldn't the redeemed patient win? or the redeem flow should put the patient into the URL like the anonymous flow does?

let parsePatient (urlParts: string list) =
urlParts
|> List.pairwise
|> List.tryFind (fun (a, b) -> a = "patient")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b is unused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants