import React, { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { Spinner } from "./components/ui/spinner"; import { JoinAccessNotice, SettingUpAccess } from "./components/JoiningPractice/JoiningPractice"; import { sessionGateFor } from "./utils/sessionGate"; import { SandboxDisabledRoute } from "./components/SandboxMode/SandboxDisabled"; import SubscriptionWarningBanner from "./components/SubscriptionWarningBanner/SubscriptionWarningBanner.tsx"; import { StalledSession, useHasStalled } from "./components/StalledSession/StalledSession"; import { DismissibleAlert } from './components/ui/dismissible-alert'; import { HashRouter as Router, Routes, Route, Navigate, Outlet, useParams, useLocation, } from "react-router-dom"; import { RedirectToSignIn, signInReturnPath } from "./components/SignInReturn/signInReturn.tsx"; import { captureConnectionReturn } from "./utils/connectionReturn"; import { Bundle, Coding, Organization } from "fhir/r4"; // services import { CommunicationServiceProvider, LocationServiceProvider, useAuthServiceContext, PatientServiceProvider, PractitionerServiceProvider, RelatedPersonServiceProvider, DuplicateMatchServiceProvider, CareViewAnchorServiceProvider, QuestionnaireServiceProvider, PublicFormServiceProvider, ActivityDefinitionServiceProvider, HealthcareServiceServiceProvider, WebSocketServiceProvider, FamilyMemberHistoryServiceProvider, useOrganizationServiceContext, AuthServiceProvider, ValueSetServiceProvider, FhirServiceProvider, OrganizationServiceProvider, useFhirServiceContext, AuditEventServiceProvider, ConsentServiceProvider, AllergyIntoleranceServiceProvider, ProcedureServiceProvider, FlagServiceProvider, ConditionServiceProvider, DeviceUseStatementServiceProvider, ObservationServiceProvider, ImmunizationServiceProvider, MedicationStatementServiceProvider, MedicationServiceProvider, CarePlanServiceProvider, CareTeamServiceProvider, DiagnosticReportServiceProvider, ImmunizationEvaluationServiceProvider, ImmunizationRecommendationServiceProvider, GoalServiceProvider, NutritionOrderServiceProvider, RiskAssessmentServiceProvider, VisionPrescriptionServiceProvider, AdverseEventServiceProvider, ClinicalImpressionServiceProvider, DetectedIssueServiceProvider, MediaServiceProvider, ImagingStudyServiceProvider, MedicationRequestServiceProvider, MedicationAdministrationServiceProvider, MedicationDispenseServiceProvider, EncounterServiceProvider, EpisodeOfCareServiceProvider, DeviceServiceProvider, DeviceRequestServiceProvider, AppointmentServiceProvider, QuestionnaireResponseServiceProvider, ScheduleServiceProvider, PractitionerRoleServiceProvider, TaskServiceProvider, PaginatorServiceProvider, DocumentReferenceServiceProvider, useWebSocketServiceContext, ConfigServiceProvider, BodyStructureServiceProvider, ListServiceProvider, StaffPermissionServiceProvider, useStaffPermissionServiceContext, CoverageServiceProvider, OrgNoteServiceProvider, InvoiceServiceProvider, PaymentServiceProvider, ChargeItemDefinitionServiceProvider, usePatientServiceContext, CollaborationTeamServiceProvider, ProofingServiceProvider, OrgAuthorizationServiceProvider, ScribeServiceProvider, EncounterNoteDraftProvider, PatientNoteDraftProvider, RecordProposalProvider, FrequentRecordItemsProvider, } from "./services"; import { FormSubmissionNotificationProvider } from "./services/FormSubmissionNotificationService.tsx"; import { PresenceServiceProvider } from './services/PresenceService'; import { AppVideoServiceProvider, useOptionalVideoServiceContext } from './services/VideoService'; import { IdleTimeoutProvider, HoldIdleWhileOnACall, } from './services/IdleTimeoutService'; import { recordSessionEnd } from './services/sessionEndReason'; import SessionEndedOverlay from './components/SessionEndedOverlay/SessionEndedOverlay'; import { BillingExportServiceProvider } from "./services/BillingExportService.tsx"; import { CampaignServiceProvider } from "./services/CampaignService.tsx"; import CampaignsPage from "./pages/DashboardPage/CampaignsPage/CampaignsPage.tsx"; import CampaignComposer from "./pages/DashboardPage/CampaignsPage/CampaignComposer.tsx"; // pages import LoginPage from "./pages/LoginPage/LoginPage.tsx"; import PracticeDoorCard from "./pages/LoginPage/PracticeDoorCard.tsx"; import PracticeContactDetails from "./pages/LoginPage/PracticeContactDetails.tsx"; import PracticeLandingPage from "./pages/PracticeLandingPage/PracticeLandingPage.tsx"; import NeutralSignupPage from "./pages/NeutralEntry/NeutralSignupPage.tsx"; import DashboardPage from "./pages/DashboardPage/DashboardPage.tsx"; import ProfilePage from "./pages/DashboardPage/ProfilePage/ProfilePage.tsx"; import PractitionerEditPatientProfilePage from "./pages/DashboardPage/PatientsPage/PatientPage/PractitionerEditPatientProfilePage/PractitionerEditPatientProfilePage.tsx"; import RecordStream from "./pages/DashboardPage/PatientsPage/PatientPage/RecordStream/RecordStream.tsx"; import VisitDetail from "./pages/DashboardPage/PatientsPage/PatientPage/RecordStream/VisitDetail.tsx"; import ResultDetail from "./pages/DashboardPage/PatientsPage/PatientPage/RecordStream/ResultDetail.tsx"; import EditProfilePage from "./pages/DashboardPage/ProfilePage/EditProfilePage/EditProfilePage.tsx"; import BillingOverviewPage from "./pages/DashboardPage/BillingPage/BillingOverviewPage.tsx"; import BillingInvoicesPage from "./pages/DashboardPage/BillingPage/BillingInvoicesPage.tsx"; import PricingOptionsPage from "./pages/DashboardPage/BillingPage/PricingOptionsPage.tsx"; import ReferralsPage from './pages/DashboardPage/ReferralsPage/ReferralsPage.tsx'; import RecordsPage from './pages/DashboardPage/RecordsPage/RecordsPage.tsx'; import RecordsConnections from './pages/DashboardPage/RecordsPage/RecordsConnections.tsx'; import RecordsConnectionReturn from './pages/DashboardPage/RecordsPage/RecordsConnectionReturn.tsx'; import MyRecordsConnections from './pages/DashboardPage/MyRecordsConnections/MyRecordsConnections.tsx'; import MyRecordsConnectionReturn from './pages/DashboardPage/MyRecordsConnections/MyRecordsConnectionReturn.tsx'; import BenchPage from './pages/DashboardPage/BenchPage/BenchPage.tsx'; import DiagnosticOrdersPage from './pages/DashboardPage/DiagnosticOrdersPage/DiagnosticOrdersPage.tsx'; import RecordDiagnosticResult from './pages/DashboardPage/DiagnosticOrdersPage/RecordDiagnosticResult/RecordDiagnosticResult.tsx'; import ReadingPage from './pages/DashboardPage/ReadingPage/ReadingPage.tsx'; import InboundRequisitionsPage from './pages/DashboardPage/InboundRequisitionsPage/InboundRequisitionsPage.tsx'; import HeldLabResultsPage from './pages/DashboardPage/HeldLabResultsPage/HeldLabResultsPage.tsx'; import BillingExportPage from "./pages/DashboardPage/BillingPage/BillingExportPage.tsx"; import PatientInvoiceList from "./pages/DashboardPage/BillingPage/PatientInvoiceList.tsx"; import MyHistoryPage from "./pages/DashboardPage/MyHistoryPage/MyHistoryPage.tsx"; import MyInvoicesPage from "./pages/DashboardPage/MyInvoicesPage/MyInvoicesPage.tsx"; import RelatedPersonsPage from "./pages/DashboardPage/RelatedPersonsPage/RelatedPersonsPage.tsx"; import EditRelatedPersonPage from "./pages/DashboardPage/RelatedPersonsPage/EditRelatedPersonPage/EditRelatedPersonPage.tsx"; import HomePage from "./pages/DashboardPage/HomePage/HomePage.tsx"; import TaskPage from "./pages/DashboardPage/HomePage/TaskPage/TaskPage.tsx"; import QuestionnairesPage from "./pages/DashboardPage/QuestionnairesPage/QuestionnairesPage.tsx"; import AutomatedTasksPage from "./pages/DashboardPage/AutomatedTasksPage/AutomatedTasksPage.tsx"; import PatientPortalPageSettings from "./pages/DashboardPage/PatientPortalPage/PatientPortalPageSettings.tsx"; import FormSubmissionNotificationsSettings from "./pages/DashboardPage/FormSubmissionNotificationsPage/FormSubmissionNotificationsSettings.tsx"; import PatientPaymentSettingsPage from "./pages/DashboardPage/PatientPaymentSettingsPage/PatientPaymentSettingsPage.tsx"; import FaxSettingsPage from "./pages/DashboardPage/FaxSettingsPage/FaxSettingsPage.tsx"; import SupportPage from "./pages/DashboardPage/SupportPage/SupportPage.tsx"; import SupportTicketDetail from "./pages/DashboardPage/SupportPage/SupportTicketDetail.tsx"; import MySupportPage from "./pages/DashboardPage/MySupportPage/MySupportPage.tsx"; import MySupportTicketDetail from "./pages/DashboardPage/MySupportPage/MySupportTicketDetail.tsx"; import AppointmentsPage from "./pages/DashboardPage/AppointmentsPage/AppointmentsPage.tsx"; import ReceptionistHomePage from "./pages/DashboardPage/ReceptionistHomePage/ReceptionistHomePage.tsx"; import ClinicianOverviewPage from "./pages/DashboardPage/ClinicianOverviewPage/ClinicianOverviewPage.tsx"; import ClinicianOverviewGate from "./components/ClinicianOverviewGate/ClinicianOverviewGate.tsx"; import { EncounterSessionProviders } from "./pages/DashboardPage/ClinicianOverviewPage/EncounterSessionProviders.tsx"; import PatientsPage from "./pages/DashboardPage/PatientsPage/PatientsPage.tsx"; import PatientImportPage from "./pages/DashboardPage/PatientImportPage/PatientImportPage.tsx"; import FormSubmissionsPage from "./pages/DashboardPage/FormSubmissionsPage/FormSubmissionsPage.tsx"; import PatientPage, { RECORD_SECTION_GROUPS } from "./pages/DashboardPage/PatientsPage/PatientPage/PatientPage.tsx"; import { DeskChartGate } from "./pages/DashboardPage/PatientsPage/PatientPage/DeskChartGate.tsx"; import SectionUnavailable from "./components/SectionUnavailable/SectionUnavailable.tsx"; import StaffOnlySection from "./components/SectionUnavailable/StaffOnlySection.tsx"; import DiagnosticsNotSetUp from "./components/SectionUnavailable/DiagnosticsNotSetUp.tsx"; import { workspaceWorksTheDiagnosticQueue } from "./utils/diagnosticOrderQueue"; import NotInThisWorkspace, { CHART_WORKSPACES, OnlyInWorkspaces, PATIENT_LIST_WORKSPACES, } from "./components/SectionUnavailable/NotInThisWorkspace.tsx"; import PatientMessagesPage from "./pages/DashboardPage/PatientMessagesPage/PatientMessagesPage.tsx"; import QuestionnairePage from "./pages/DashboardPage/QuestionnairesPage/QuestionnairePage/QuestionnairePage.tsx"; import MessagesPage from "./pages/DashboardPage/MessagesPage/MessagesPage.tsx"; import QuestionnaireSearchPage from "./pages/DashboardPage/QuestionnairesPage/QuestionnaireSearchPage/QuestionnaireSearchPage.tsx"; import FamilyMemberHistories from "./pages/DashboardPage/ClinicalDataPage/FamilyMembersHistory/FamilyMemberHistories.tsx"; import EditFamilyMemberHistoryPage from "./pages/DashboardPage/ClinicalDataPage/FamilyMembersHistory/FamilyMemberHistoryPage/EditFamilyMemberHistoryPage/EditFamilyMemberHistoryPage.tsx"; import OrganizationSetupPage from "./pages/OrganizationSetupPage/OrganizationSetupPage.tsx"; import DetachInstallationCode from "./pages/OrganizationSetupPage/DetachInstallationCode.tsx"; import { isAdoptedInstallationCode } from "./utils/wpAdminApi.ts"; import AcceptCareTeamInvitePage from "./pages/AcceptCareTeamInvitePage/AcceptCareTeamInvitePage.tsx"; import CredentialRevertPage from "./pages/CredentialRevertPage/CredentialRevertPage.tsx"; import InvoicePayPage from "./pages/InvoicePayPage/InvoicePayPage.tsx"; import { PAY_LINK_PATH } from "./utils/invoicePayLinkToken"; import { CrossPortalGate } from "./components/CrossPortalGate/CrossPortalGate.tsx"; import { AgencyTermsGate } from "./components/AgencyTermsGate/AgencyTermsGate.tsx"; import CredentialUnlockPage from "./pages/CredentialUnlockPage/CredentialUnlockPage.tsx"; import PhoneRecoveryPage from "./pages/PhoneRecoveryPage/PhoneRecoveryPage.tsx"; import AuditEventsPage from "./pages/DashboardPage/AuditEventsPage/AuditEventsPage.tsx"; import ReportsPage from "./pages/DashboardPage/ReportsPage/ReportsPage.tsx"; import SchedulingReportPage from "./pages/DashboardPage/ReportsPage/SchedulingReportPage.tsx"; import PracticeReportPage from "./pages/DashboardPage/ReportsPage/PracticeReportPage.tsx"; import PaymentSuccessPage from "./pages/PaymentSuccessPage/PaymentSuccessPage.tsx"; import PaymentErrorPage from "./pages/PaymentErrorPage/PaymentErrorPage.tsx"; import SubscriptionPage from "./pages/SubscriptionPage/SubscriptionPage.tsx"; import StaffMessagesPage from "./pages/DashboardPage/StaffMessagesPage/StaffMessagesPage.tsx"; import { ClinicianInvitationLanding } from "./components/ClinicianInvitationNotice/ClinicianInvitationEntry.tsx"; import { OwnerAcceptLanding } from "./components/OwnerAccept/OwnerAcceptPage.tsx"; import ClinicalDataPage from "./pages/DashboardPage/ClinicalDataPage/ClinicalDataPage.tsx"; import EditAllergyIntolerancePage from "./pages/DashboardPage/ClinicalDataPage/AllergyIntolerances/AllergyIntolerancePage/EditAllergyIntolerancePage/EditAllergyIntolerancePage.tsx"; import PatientMedicationsPage from "./pages/DashboardPage/PatientMedicationsPage/PatientMedicationsPage.tsx"; import MedicationsOverview from "./pages/DashboardPage/PatientMedicationsPage/MedicationsOverview/MedicationsOverview.tsx"; import CarePlansOverview from "./pages/DashboardPage/CareProvisionPage/CarePlansOverview/CarePlansOverview.tsx"; import EditConditionPage from "./pages/DashboardPage/ClinicalDataPage/Conditions/ConditionPage/ConditionPage.tsx"; import EditProcedurePage from "./pages/DashboardPage/ClinicalDataPage/Procedures/Procedure/Procedure.tsx"; import AllergyIntolerances from "./pages/DashboardPage/ClinicalDataPage/AllergyIntolerances/AllergyIntolerances.tsx"; import Conditions from "./pages/DashboardPage/ClinicalDataPage/Conditions/Conditions.tsx"; import Procedures from "./pages/DashboardPage/ClinicalDataPage/Procedures/Procedures.tsx"; import DiagnosticsDataPage from "./pages/DashboardPage/DiagnosticsDataPage/DiagnosticsDataPage.tsx"; import Observations from "./pages/DashboardPage/DiagnosticsDataPage/Observations/Observations.tsx"; import EditObservationPage from "./pages/DashboardPage/DiagnosticsDataPage/Observations/ObservationPage/ObservationPage.tsx"; import Immunizations from "./pages/DashboardPage/PatientMedicationsPage/Immunizations/Immunizations.tsx"; import EditImmunizationPage from "./pages/DashboardPage/PatientMedicationsPage/Immunizations/ImmunizationPage/ImmunizationPage.tsx"; import MedicationStatements from "./pages/DashboardPage/PatientMedicationsPage/MedicationStatements/MedicationStatements.tsx"; import EditMedicationStatementPage from "./pages/DashboardPage/PatientMedicationsPage/MedicationStatements/MedicationStatementPage/MedicationStatementPage.tsx"; import MedicationsPage from "./pages/DashboardPage/MedicationsPage/MedicationsPage.tsx"; import EditMedicationPage from "./pages/DashboardPage/MedicationsPage/MedicationPage/MedicationPage.tsx"; import CareProvisionPage from "./pages/DashboardPage/CareProvisionPage/CareProvisionPage.tsx"; import CarePlans from "./pages/DashboardPage/CareProvisionPage/CarePlans/CarePlans.tsx"; import EditCarePlanPage from "./pages/DashboardPage/CareProvisionPage/CarePlans/CarePlanPage/CarePlanPage.tsx"; import CareTeams from "./pages/DashboardPage/CareProvisionPage/CareTeams/CareTeams.tsx"; import EditCareTeamPage from "./pages/DashboardPage/CareProvisionPage/CareTeams/CareTeamPage/CareTeamPage.tsx"; import OutsideProviders from "./pages/DashboardPage/CareProvisionPage/OutsideProviders/OutsideProviders.tsx"; import DiagnosticReports from "./pages/DashboardPage/DiagnosticsDataPage/DiagnosticReports/DiagnosticReports.tsx"; import DiagnosticReportRoute from "./pages/DashboardPage/DiagnosticsDataPage/DiagnosticReports/DiagnosticReportPage/DiagnosticReportRoute.tsx"; import ImmunizationEvaluations from "./pages/DashboardPage/PatientMedicationsPage/ImmunizationEvaluations/ImmunizationEvaluations.tsx"; import EditImmunizationEvaluationPage from "./pages/DashboardPage/PatientMedicationsPage/ImmunizationEvaluations/ImmunizationEvaluationPage/ImmunizationEvaluationPage.tsx"; import ImmunizationRecommendations from "./pages/DashboardPage/PatientMedicationsPage/ImmunizationRecommendations/ImmunizationRecommendations.tsx"; import EditImmunizationRecommendationPage from "./pages/DashboardPage/PatientMedicationsPage/ImmunizationRecommendations/ImmunizationRecommendationPage/ImmunizationRecommendationPage.tsx"; import Goals from "./pages/DashboardPage/CareProvisionPage/Goals/Goals.tsx"; import GoalRouteRedirect from "./pages/DashboardPage/CareProvisionPage/Goals/GoalRouteRedirect.tsx"; import StaffRecordView from "./pages/DashboardPage/PatientsPage/PatientPage/StaffRecord/StaffRecordView.tsx"; import NutritionOrders from "./pages/DashboardPage/CareProvisionPage/NutritionOrders/NutritionOrders.tsx"; import PatientReferrals from "./pages/DashboardPage/CareProvisionPage/Referrals/Referrals.tsx"; import PatientDiagnosticOrders from "./pages/DashboardPage/CareProvisionPage/DiagnosticOrders/DiagnosticOrders.tsx"; import EditNutritionOrderPage from "./pages/DashboardPage/CareProvisionPage/NutritionOrders/NutritionOrderPage/NutritionOrderPage.tsx"; import RiskAssessments from "./pages/DashboardPage/CareProvisionPage/RiskAssessments/RiskAssessments.tsx"; import EditRiskAssessmentPage from "./pages/DashboardPage/CareProvisionPage/RiskAssessments/RiskAssessmentPage/RiskAssessmentPage.tsx"; import VisionPrescriptions from "./pages/DashboardPage/CareProvisionPage/VisionPrescriptions/VisionPrescriptions.tsx"; import EditVisionPrescriptionPage from "./pages/DashboardPage/CareProvisionPage/VisionPrescriptions/VisionPrescriptionPage/VisionPrescriptionPage.tsx"; import AdverseEvents from "./pages/DashboardPage/ClinicalDataPage/AdverseEvents/AdverseEvents.tsx"; import EditAdverseEventPage from "./pages/DashboardPage/ClinicalDataPage/AdverseEvents/AdverseEventPage/AdverseEventPage.tsx"; import ClinicalImpressions from "./pages/DashboardPage/ClinicalDataPage/ClinicalImpressions/ClinicalImpressions.tsx"; import EditClinicalImpressionPage from "./pages/DashboardPage/ClinicalDataPage/ClinicalImpressions/ClinicalImpressionPage/ClinicalImpressionPage.tsx"; import DetectedIssues from "./pages/DashboardPage/ClinicalDataPage/DetectedIssues/DetectedIssues.tsx"; import EditDetectedIssuePage from "./pages/DashboardPage/ClinicalDataPage/DetectedIssues/DetectedIssuePage/DetectedIssuePage.tsx"; import Media from "./pages/DashboardPage/DiagnosticsDataPage/Media/Media.tsx"; import EditMediaPage from "./pages/DashboardPage/DiagnosticsDataPage/Media/MediaPage/MediaPage.tsx"; import ImagingStudies from "./pages/DashboardPage/DiagnosticsDataPage/ImagingStudies/ImagingStudies.tsx"; import ImagingStudyViewer from "./pages/DashboardPage/DiagnosticsDataPage/ImagingStudies/ImagingStudyViewer.tsx"; import MedicationRequests from "./pages/DashboardPage/PatientMedicationsPage/MedicationRequests/MedicationRequests.tsx"; import EditMedicationRequestPage from "./pages/DashboardPage/PatientMedicationsPage/MedicationRequests/MedicationRequestPage/MedicationRequestPage.tsx"; import MedicationAdministrations from "./pages/DashboardPage/PatientMedicationsPage/MedicationAdministrations/MedicationAdministrations.tsx"; import EditMedicationAdministrationPage from "./pages/DashboardPage/PatientMedicationsPage/MedicationAdministrations/MedicationAdministrationPage/MedicationAdministrationPage.tsx"; import MedicationDispenses from "./pages/DashboardPage/PatientMedicationsPage/MedicationDispenses/MedicationDispenses.tsx"; import EditMedicationDispensePage from "./pages/DashboardPage/PatientMedicationsPage/MedicationDispenses/MedicationDispensePage/MedicationDispensePage.tsx"; import PatientManagementPage from "./pages/DashboardPage/PatientManagementPage/PatientManagementPage.tsx"; import Encounters from "./pages/DashboardPage/PatientManagementPage/Encounters/Encounters.tsx"; import EditEncounterPage from "./pages/DashboardPage/PatientManagementPage/Encounters/EncounterPage/EncounterPage.tsx"; import EpisodesOfCare from "./pages/DashboardPage/PatientManagementPage/EpisodesOfCare/EpisodesOfCare.tsx"; import EditEpisodeOfCarePage from "./pages/DashboardPage/PatientManagementPage/EpisodesOfCare/EpisodeOfCarePage/EpisodeOfCarePage.tsx"; import CoverageListPage from "./pages/DashboardPage/InsurancePage/CoverageListPage/CoverageListPage.tsx"; import PatientNotes from "./components/PatientNotes/PatientNotes.tsx"; import PatientAccessLog from "./pages/DashboardPage/PatientsPage/PatientPage/PatientAccessLog/PatientAccessLog.tsx"; import PatientSnapshot from "./components/PatientSnapshot/PatientSnapshot.tsx"; import ErrorBoundary from "./components/ErrorBoundary/ErrorBoundary.tsx"; import { demoTabAtLoad } from "./services/authStorage"; import { demoClinicOrganization } from "./utils/demoClinic"; import { PortalFrameHeader } from "./components/PortalFrameHeader/PortalFrameHeader.tsx"; import FrontDeskHeaderActions from "./components/PortalFrameHeader/FrontDeskHeaderActions.tsx"; import { frontDeskActionGrants } from "./components/PortalFrameHeader/frontDeskHeaderAction"; import { useClosedToStaff } from "./services/PracticeClosureService"; import { BookAppointmentDialogProvider } from "./components/BookAppointmentDialog/BookAppointmentDialogProvider.tsx"; import { PracticeSwitcherProvider } from "./components/PracticeSwitcher/PracticeSwitcherProvider.tsx"; import { PatientInboxProvider } from "./components/PatientInbox/PatientInboxProvider.tsx"; import SetupHelpWidgetMount from "./components/SetupHelpWidget/SetupHelpWidgetMount"; import { SandboxModeEscape } from "./components/SandboxMode/SandboxModeEscape.tsx"; import { SandboxBanner } from "./components/SandboxBanner/SandboxBanner.tsx"; import { TenantGate } from "./components/SandboxBanner/TenantGate.tsx"; import { ViewerScope } from "./components/ViewerScope/ViewerScope.tsx"; import { PatientFrameSecondRow, useIsPatientViewer, FrameIdentity, } from "./components/PortalFrameHeader/patientFrameSlots.tsx"; import AnchorList from "./components/AnchorView/AnchorList.tsx"; import AnchorView from "./components/AnchorView/AnchorView.tsx"; import DeviceDataPage from "./pages/DashboardPage/DeviceDataPage/DeviceDataPage.tsx"; import Devices from "./pages/DashboardPage/DeviceDataPage/Devices/Devices.tsx"; import EditDevicePage from "./pages/DashboardPage/DeviceDataPage/Devices/DevicePage/DevicePage.tsx"; import DeviceUseStatements from "./pages/DashboardPage/DeviceDataPage/DeviceUseStatements/DeviceUseStatements.tsx"; import EditDeviceUseStatementPage from "./pages/DashboardPage/DeviceDataPage/DeviceUseStatements/DeviceUseStatement/DeviceUseStatement.tsx"; import DeviceRequests from "./pages/DashboardPage/DeviceDataPage/DeviceRequests/DeviceRequests.tsx"; import EditDeviceRequestPage from "./pages/DashboardPage/DeviceDataPage/DeviceRequests/DeviceRequestPage/DeviceRequestPage.tsx"; import PatientTasks from "./pages/DashboardPage/PatientsPage/PatientPage/PatientTasks/PatientTasks.tsx"; import PatientTaskPage from "./pages/DashboardPage/PatientsPage/PatientPage/PatientTasks/PatientTaskPage/PatientTaskPage.tsx"; import PatientAppointments from "./pages/DashboardPage/PatientsPage/PatientPage/PatientAppointments/PatientAppointments.tsx"; import PatientQuestionnaireResponses from "./pages/DashboardPage/PatientsPage/PatientPage/PatientQuestionnaireResponses/PatientQuestionnaireResponses.tsx"; import SchedulesPage from "./pages/DashboardPage/SchedulesPage/SchedulesPage.tsx"; import ConsentsPage from "./pages/DashboardPage/ConsentsPage/ConsentsPage.tsx"; import EditConsentPage from "./pages/DashboardPage/ConsentsPage/ConsentPage/EditConsentPage/EditConsentPage.tsx"; import PatientSettingsPage from "./pages/DashboardPage/PatientSettingsPage/PatientSettingsPage.tsx"; import MyProvidersPage from "./pages/DashboardPage/MyProvidersPage/MyProvidersPage.tsx"; import ProviderDeepLinkRedirect from "./pages/DashboardPage/MyProvidersPage/ProviderDeepLinkRedirect.tsx"; import CareTeamsPage from "./pages/DashboardPage/CareTeamsPage/CareTeamsPage.tsx"; import StaffCareTeamsPage from "./pages/DashboardPage/StaffCareTeamsPage/StaffCareTeamsPage.tsx"; import OrganizationPage from "./pages/DashboardPage/OrganizationPage/OrganizationPage.tsx"; import LocationsPage from "./pages/DashboardPage/OrganizationPage/LocationsPage/LocationsPage.tsx"; import HealthcareServicesPage from "./pages/DashboardPage/OrganizationPage/HealthcareServicesPage/HealthcareServicesPage.tsx"; import EditLocationPage from "./pages/DashboardPage/OrganizationPage/LocationsPage/LocationPage/LocationPage.tsx"; import EditHealthcareServicePage from "./pages/DashboardPage/OrganizationPage/HealthcareServicesPage/HealthcareServicePage/HealthcareServicePage.tsx"; import BaaPage from "./pages/DashboardPage/OrganizationPage/BaaPage/BaaPage.tsx"; import MsaAcceptPage from "./pages/DashboardPage/OrganizationPage/BaaPage/MsaAcceptPage.tsx"; import { MsaRecoveryAccess } from "./pages/DashboardPage/OrganizationPage/BaaPage/MsaRecovery"; import { useOrganizationOwnerService } from "./services/OrganizationOwnerService"; import { referencesPractitioner } from "./components/OrganizationOwnerNotice/ownerReference"; import DiagnosticsPage from "./pages/DashboardPage/OrganizationPage/DiagnosticsPage/DiagnosticsPage.tsx"; import BookingAndRemindersPage from "./pages/DashboardPage/OrganizationPage/BookingAndRemindersPage.tsx"; import CampaignSendingDomainPage from "./pages/DashboardPage/CampaignsPage/CampaignSendingDomainPage.tsx"; import ReferralsSettingsPage from "./pages/DashboardPage/OrganizationPage/ReferralsSettingsPage.tsx"; import AgreementsPage from "./pages/DashboardPage/OrganizationPage/AgreementsPage.tsx"; import ClinicianReadinessPage from "./pages/DashboardPage/OrganizationPage/ClinicianReadinessPage.tsx"; import PracticeAccountPage from "./pages/DashboardPage/OrganizationPage/PracticeAccountPage.tsx"; import PracticeClosurePage from "./pages/DashboardPage/OrganizationPage/PracticeClosure/PracticeClosurePage.tsx"; import { staffRecordRoutes } from "./pages/DashboardPage/UsersPage/UserPage/staffRecordRoutes.tsx"; import SignupPage from "./pages/SignupPage/SignupPage.tsx"; import UsersPage from "./pages/DashboardPage/UsersPage/UsersPage.tsx"; import NewUserPage from "./pages/DashboardPage/UsersPage/NewUserPage/NewUserPage.tsx"; import RolePermissionsPage from "./pages/DashboardPage/UsersPage/RolePermissionsPage/RolePermissionsPage.tsx"; import DocumentReferences from "./pages/DashboardPage/DocumentReferences/DocumentReferences.tsx"; import EditDocumentReferencePage from "./pages/DashboardPage/DocumentReferences/DocumentReference/DocumentReference.tsx"; import PatientQuestionnaireResponse from "./pages/DashboardPage/PatientsPage/PatientPage/PatientQuestionnaireResponses/PatientQuestionnaireResponse/PatientQuestionnaireResponse.tsx"; import BodyStructures from "./pages/DashboardPage/DiagnosticsDataPage/BodyStructures/BodyStructures.tsx"; import EditBodyStructurePage from "./pages/DashboardPage/DiagnosticsDataPage/BodyStructures/BodyStructurePage/BodyStructurePage.tsx"; // billing import { BillingProvider, useBillingContext } from './services/BillingContext.tsx'; import BillingLimitModal from './components/BillingLimitModal/BillingLimitModal.tsx'; // video import { CallOverlay } from './components/video/CallOverlay'; // first-sign-in gate ( Plan B, client Task 4). AkashicAgreementsServiceProvider is imported // directly, not via the./services barrel: the barrel (./services/index.ts) does not re-export it. // This is also the actual PROVIDER MOUNT (final review fix round, finding 1) - before this, nothing // in src/ rendered AkashicAgreementsServiceProvider anywhere, so the gate component's unconditional // useAkashicAgreementsServiceContext() call threw on every first paint, above every ErrorBoundary. // See FirstSignInGateProviderWiring.test.tsx for the regression test against the real provider. // (Note for anyone grepping: the literal JSX tag below is deliberately the ONLY occurrence of that // exact marker string in this file - firstSignInGateMount.test.tsx's Part A depends on that.) import FirstSignInGate from './components/FirstSignInGate/FirstSignInGate.tsx'; import { AkashicAgreementsServiceProvider } from './services/AkashicAgreementsService.tsx'; import { FirstSignInGateStatusProvider } from './components/FirstSignInGate/firstSignInGateStatus.tsx'; import { CapabilityServiceProvider, useCapabilityServiceContext } from './services/CapabilityService.tsx'; import { TenantServiceProvider, useTenant } from './services/TenantService.tsx'; // styles import "./App.css"; import { environment } from "./environments/environment.ts"; import { Toaster } from "sonner"; import type { StaffPermissionSection } from "./utils/staffAccess.ts"; import type { StaffWorkspace } from "./utils/staffRoleCatalog.ts"; import { getMountContext, isPatientExperience, isStaffExperience, isWpAdminMount } from "./utils/mountContext.ts"; import { defaultDashboardPathFor } from "./utils/staffLanding.ts"; import { billingGateDecision } from "./utils/billingGateDecision.ts"; import { formatSingleAddress } from "./utils/fhirAddress"; import { isStaffMessagingWorkspace } from './utils/staffWorkspaceAccess'; import { classifyOrgFetchFailure } from './utils/orgFetchFailure'; import { hostedLabel, hostedMountContext, isHostedMount, isOrglessPatientsHost, rootSiteUrl } from './utils/hostedMount'; import { captureCampaignCtaAttribution } from './utils/campaignCtaAttribution'; import { NotificationLinkOpener } from './components/NotificationLink/NotificationLinkOpener'; import { patientPortalEnabled } from './utils/patientPortalAccess'; import { OverlayViewerContext } from './lib/overlayPresentation'; import { isAdolescentSelf } from './utils/adolescentFeatureGate'; import { SignupDetailsCompletion } from './components/SignupDetailsCompletion/SignupDetailsCompletion'; // import { // FhirOrganizationDisplay, // ProfileLayoutContainer, // ScrollableTab, // } from "./components"; interface ServiceProvidersProps { children: ReactNode; } interface FhirCloudEvent { type: string; data: T; } // EVERY GATE IN THIS FILE RENDERS THE SAME SPINNER, AND THAT COST FIVE WALKS. // // Ten call sites, byte-identical DOM. When a screen hangs on one of them the rendered page cannot say // WHICH - so diagnosing "the staff portal spins forever" meant reasoning from source about a branch // nobody could observe, four times, across four different fixes for one symptom. // // `gate` is the name of the condition that is waiting, written into the DOM. It answers the only // question a hung screen is ever asked: which of these ten is it. Cheap, permanent, and it needs no // session to read. const LoadingSpinner = ({ gate, message = "Loading…" }: { gate: string; message?: string }) => (

{message}

); const BillingWebSocketHandler: React.FC = () => { const { subscribe, unsubscribe } = useWebSocketServiceContext(); const { refreshBilling } = useBillingContext(); const { currentUser } = useAuthServiceContext(); useEffect(() => { if (!currentUser) return; const handleBillingEvent = () => { refreshBilling(); }; // A CLINICIAN SEAT LIVES ON A PractitionerRole, NOT ON THE ORGANIZATION. // // This handler listened for `organization.updated` alone, so the one write that moves the bill // most often - granting or removing a seat - never reached it. An admin who removed a seat to // stop paying and then opened Subscription was shown their previous bill, and the obvious // conclusion is that the removal did not take. // // The seat count and every per-clinician price are derived at read time, so a refetch is all // that was ever needed; nothing was stale on the server. Subscribing here rather than refetching // on mount keeps an ordinary visit free, and covers the case a direct call cannot: another // administrator changing a seat while this screen is open. const BILLING_EVENTS = [ 'com.akashichealthrecords.fhir.organization.updated', 'com.akashichealthrecords.fhir.practitionerrole.created', 'com.akashichealthrecords.fhir.practitionerrole.updated', 'com.akashichealthrecords.fhir.practitionerrole.deleted', ]; BILLING_EVENTS.forEach((event) => subscribe(event, handleBillingEvent)); return () => { BILLING_EVENTS.forEach((event) => unsubscribe(event, handleBillingEvent)); }; }, [currentUser, subscribe, unsubscribe, refreshBilling]); return null; }; // No billing state replaces the app; the banner above it says what changed (see billingGateDecision). const BillingGate: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { isLoading } = useBillingContext(); if (billingGateDecision({ isLoading }) === 'loading') { return ; } // The warning sits at the TOP of the view while the app still works. return ( <> {children} ); }; /** * - the 60-minute idle timeout, mounted once for the whole signed-in app. * * `enabled` is the signed-in state, not a constant: there is nothing to time out on the login page, * and a clock running there would sign somebody out of a session they had not started. * * THE SIGN-OUT MUST CLEAR THE STORED TOKENS, which is why it calls `logout()` rather than navigating. * A redirect alone leaves the tokens in `localStorage` and the timeout is cosmetic - the next tab, or the * next person at a shared front desk, is still signed in. * * The flag is written BEFORE `logout()` because `logout()` ends in `window.location.reload()`, so * anything after it does not run. `sessionStorage`, not `localStorage`: the explanation belongs to this * tab and this moment, and must not greet somebody who opens a new tab tomorrow. */ const IdleTimeout = ({ children }: { children: React.ReactNode }) => { const { isLoggedIn, logout } = useAuthServiceContext(); const video = useOptionalVideoServiceContext(); // ONE MECHANISM FOR EVERY INVOLUNTARY EXIT, and this was the only one that ever had a reason. // A second flag beside it is how two alerts end up rendering at once, each describing a different // cause for the same sign-out. The sentence a person reads here is unchanged. const signOutForInactivity = React.useCallback(() => { recordSessionEnd('inactivity'); logout(); }, [logout]); return ( {children} {/* MOUNTED BESIDE THE IDLE TIMER BECAUSE IT IS THE OTHER HALF OF IT. The timer is per-tab and in-memory, so the tab that goes quiet first signs the machine out and reloads itself; the rest keep the chart on screen with the tokens already gone. This watches the token key and covers the content rather than redirecting, so nothing unsaved is discarded on a tab whose owner is not there to be asked. Rendered unconditionally rather than under `isLoggedIn`: this tab still believes it is signed in - that belief is the defect - so gating on it would hide the cover in exactly the state it exists for. */} ); }; const OrganizationCheck = ({ children }: { children: React.ReactNode }) => { const installationId = environment.wpData?.installationId; const context = environment.wpData?.context; const { currentUser, currentOrganization, wpOrganization, setWpOrganization, logout, } = useAuthServiceContext(); const { subscribe, unsubscribe } = useWebSocketServiceContext(); const { fetchWordPressOrganization, fetchOrganization, fetchOrganizationAsCaller, getOrganization, setOrganization, resolvePracticeHostname, } = useOrganizationServiceContext(); const { unpackBundle } = useFhirServiceContext(); const [isLoading, setIsLoading] = useState(true); const [organizationExists, setOrganizationExists] = useState(false); const [error, setError] = useState(null); // Spencer's ruling, 2026-08-05: "transport failure shouldn't log anyone out." Kept as its own state // rather than folded into `error`, because the two outcomes need opposite treatment - `error` ends // the session, this one must leave it entirely alone and offer a retry. const [unreachable, setUnreachable] = useState(false); // ASSIGNED ON MOUNT, NOT JUST CLEARED ON UNMOUNT - same shape and same reason as // CredentialUnlockPage's. src/main.tsx renders inside , and WordPress serves // React's DEVELOPMENT build whenever SCRIPT_DEBUG is on, so effects double-invoke (mount cleanup // mount) while PRESERVING refs. An unmount-only flag would still read false on the second mount and // silently swallow every state update below. const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const checkOrganization = useCallback(async () => { setError(null); setUnreachable(false); /** * THE HOSTED TARGET ASKS THE SERVER WHICH PRACTICE THIS ADDRESS IS. There is no WordPress here, so * there is no `installationId` - and the branch below would refuse every hosted visitor with a * sentence about a WordPress site they have never seen. * * The resolve is public and runs before anybody signs in, because the sign-in screen itself carries * the practice's name and logo. * * A refusal here must not end a session, and that is the one place this branch deliberately parts * company with the WordPress one below. There, the server answering "no" means a bad session or an org * this installation cannot use, and `logout()` is correct. Here it means nobody has claimed this * address - which says nothing at all about whoever is looking at it, and `logout()` ends in * `window.location.reload()`, so an unclaimed hostname would reload itself forever. */ const demoClinic = demoClinicOrganization(demoTabAtLoad); if (demoClinic) { setOrganizationExists(true); setOrganization(demoClinic); setWpOrganization(demoClinic); setIsLoading(false); return; } if (isOrglessPatientsHost()) { // No practice to resolve: the session belongs to the patient alone. setOrganizationExists(true); setWpOrganization(null); setIsLoading(false); return; } if (isHostedMount()) { const label = hostedLabel(); if (!label) { setOrganizationExists(false); setIsLoading(false); setWpOrganization(null); return; } try { const resolved = await resolvePracticeHostname(label); // Organization-shaped from the start, because every consumer below already reads a FHIR // Organization. `branding` is the allow-list of presentation extensions, which is what // the sign-in screen reads its logo from. const organization = { resourceType: 'Organization', id: resolved.organizationId, name: resolved.name, extension: resolved.branding ?? [], } as Organization; if (!mountedRef.current) return; setOrganizationExists(true); setOrganization(organization); setWpOrganization(organization); } catch (error) { if (!mountedRef.current) return; // Same ruling as below, and it matters more here: the caller is anonymous and a blip must // not turn into a screen saying this practice does not exist. if (classifyOrgFetchFailure(error) === 'unreachable') { console.error("Could not reach the server to resolve this address:", error); setUnreachable(true); return; } // The server ANSWERED: nobody holds this hostname. A denial, not a failure - and not a // reason to touch anybody's session. // It goes to the root site, the same way a reserved address does; the alert below is // only for a host with no parent domain. const root = rootSiteUrl(); if (root) { window.location.replace(root); return; } setOrganizationExists(false); setWpOrganization(null); } finally { if (mountedRef.current) setIsLoading(false); } return; } if (!installationId) { setOrganizationExists(false); setIsLoading(false); setWpOrganization(null); return; } const organizationId = getOrganization(); let organization: Organization | undefined; try { if (organizationId) { const response: Organization = await fetchOrganization( organizationId, installationId, ); organization = response ? response : undefined; } else { const bundle: Bundle = await fetchWordPressOrganization(installationId); const organizations: Organization[] = unpackBundle(bundle); organization = organizations.find((org: Organization) => (org.meta?.tag ?? []).find( (tag: Coding) => tag.code === installationId, ), ) as Organization; } if (organization) { setOrganizationExists(true); setOrganization(organization); setWpOrganization(organization); } else { setOrganizationExists(false); } } catch (error) { // AN UNMOUNTED OrganizationCheck MUST NEVER END A SESSION. This fetch is async and nothing // cancels it, so a request started here can still reject after the user has navigated to a // route where this component is deliberately NOT mounted - most importantly the two // credential-recovery paths, which hoisted ABOVE OrganizationCheck precisely so an org // failure could never strand a patient outside their own record. Without this guard that hoist // is bypassed: logout() runs, window.location.reload() fires, and the recovery page is torn // down by a reload it was specifically made immune from. CredentialRevertPage acts ON LOAD with // a SINGLE-USE token, so that reload can leave a patient holding a dead "wasn't me" link. if (!mountedRef.current) return; // SPENCER'S RULING, 2026-08-05: "transport failure shouldn't log anyone out." // A dropped connection is not a refusal - we learned NOTHING, so nothing may be destroyed on // the strength of it. Previously every failure landed below and called logout(), which ends in // window.location.reload(): one blip during app load signed the user out and hard-reloaded the // page. See utils/orgFetchFailure.ts for why this is `instanceof` and not a duck-type, and // for the class. if (classifyOrgFetchFailure(error) === 'unreachable') { console.error("Could not reach the server to load the organization:", error); setUnreachable(true); return; } // The server ANSWERED and said no - a bad session or an org this installation cannot use. // Ending the session here is the pre-existing, deliberate behaviour and is unchanged. console.error("Organization not found:", error); setError("Could not load this practice. Try again."); setWpOrganization(null); logout(); } finally { setIsLoading(false); } }, [fetchWordPressOrganization, unpackBundle, installationId, setWpOrganization, getOrganization, fetchOrganization, setOrganization, logout, resolvePracticeHostname]); useEffect(() => { void checkOrganization(); }, [checkOrganization]); /** * - ONCE THERE IS A TOKEN, READ THE ORGANIZATION AGAIN AS OURSELVES. * * The bootstrap above runs before anyone signs in, so it reads `bypassAuth` and the API answers with * the **public projection**: no `meta.versionId`, no `stripeCustomerId` tag, no subscription, and none * of the practice's configuration. Every screen that saves the organization builds its PUT from this * copy, and the server takes an admin's PUT literally saving from a projected copy writes a document * missing everything the projection dropped. * * `meta.versionId` IS THE TELL AND THE IDEMPOTENCE. HealthLake stamps it on everything it serves and * the projection strips it, so "no versionId" means "this is not a record we read" - the same test the * onboarding writer now refuses on. Setting the full record clears the condition, so this runs once. * * IT FAILS SOFT ON PURPOSE. A failed re-read leaves the projected copy in place, which is exactly * today's behaviour - the app still renders, patients still reach the portal. Ending a session over it * would be a harder failure than the one being fixed Spencer's 2026-08-05 ruling that a transport * failure must not log anyone out. */ useEffect(() => { const organizationId = currentOrganization?.id; // A HOSTED PRACTICE HAS NO INSTALLATION ID, and requiring one here is what left its staff holding // the anonymous projection for a whole session - the copy every save builds its PUT from. if (!currentUser || !organizationId) return; if (!installationId && !isHostedMount()) return; if (currentOrganization?.meta?.versionId) return; let cancelled = false; void (async () => { try { const full = await fetchOrganizationAsCaller(organizationId, installationId); if (cancelled || !mountedRef.current) return; // Another writer may have replaced the org while this was in flight; theirs is newer. if (wpOrganization.current?.id !== organizationId) return; if (wpOrganization.current?.meta?.versionId) return; setWpOrganization(full); } catch (error) { console.error("Could not re-read the organization as the signed-in user:", error); } })(); return () => { cancelled = true; }; }, [ currentUser, currentOrganization?.id, currentOrganization?.meta?.versionId, installationId, fetchOrganizationAsCaller, setWpOrganization, wpOrganization, ]); useEffect(() => { if (!currentUser) { return; } const handleOrganizationEvent = (cloudEvent: FhirCloudEvent) => { const organization: Organization = cloudEvent.data; const eventType = cloudEvent.type; switch (eventType) { case "com.akashichealthrecords.fhir.organization.updated": handleOrganizationUpdate(organization); break; } }; const handleOrganizationUpdate = (organization: Organization) => { if ( !organization.id || (wpOrganization.current?.id && organization.id !== wpOrganization.current.id) ) { return; } // The broadcast payload can be partial - an unrelated update (e.g. appointment policies) may omit extensions // the client depends on, most critically `baa-status`, which gates frontend patient-portal access. Merge the // payload onto the current org and merge extensions BY URL so a policy save can never drop the BAA/subscription // extensions and bounce a patient out of the portal. A genuine change (e.g. a BAA revoke) is present in the // payload and still wins. const current = wpOrganization.current; const incomingExtensions = organization.extension ?? []; const incomingUrls = new Set(incomingExtensions.map((e) => e.url)); const preservedExtensions = (current?.extension ?? []).filter( (e) => e.url && !incomingUrls.has(e.url), ); const mergedExtensions = [...incomingExtensions, ...preservedExtensions]; setWpOrganization({ ...current, ...organization, extension: mergedExtensions.length ? mergedExtensions : undefined, }); }; subscribe( "com.akashichealthrecords.fhir.organization.updated", handleOrganizationEvent, ); return () => { unsubscribe( "com.akashichealthrecords.fhir.organization.updated", handleOrganizationEvent, ); }; }, [ currentUser, subscribe, unsubscribe, wpOrganization, setWpOrganization, ]); if (isLoading) { return ; } // NOT A REFUSAL SCREEN, and deliberately NOT the `error` branch below. We never got an answer, so // the session is untouched and the only honest thing to say is that we could not reach us. It says so // explicitly ("you have not been signed out") because the previous behaviour DID sign them out, and a // patient who has seen that once will assume it happened again. // Radix/Tailwind + `btn btn-blue`, not the WordPress used by the branches around it - zero // NEW WordPress per the standing rule; the existing usages are outside this change's blast radius. // Mirrors CredentialRevertPage/CredentialUnlockPage's own 'unreachable' state, which is where this // transport-vs-refusal distinction was first established. if (unreachable) { return (

We could not reach Akashic just now.

This is a connection problem, not a problem with your account — you have not been signed out. Please try again in a moment.

); } if (error) { return (
{error}
); } /* * THE WAY BACK OFF A PRACTICE, and it can only live here because this is the only branch an ATTACHED * site reaches. The setup screen renders exactly when no organization exists, so a site that has * adopted a code never sees it again - and the adopt control, the refusal that protects somebody * else's practice, and the explanation all live on that screen. * * SHAPE IS WHAT TELLS THE TWO KINDS OF SITE APART. An id the plugin generated for itself is an md5; a * site that took a contractor's code is running that code verbatim. Offering the control to a site * with nothing to disconnect would invite somebody to break a connection that was never made. * * ADMIN MOUNT ONLY. Deciding which practice a website belongs to is a site-owner act - the server * asks for `manage_options` - and it has no business on a patient's or a receptionist's screen. */ const adoptedCode = isAdoptedInstallationCode(installationId) ? installationId : undefined; if (!organizationExists) { if (isWpAdminMount()) { return ; } /* NAME WHAT IS DENIED, AND THE TWO TARGETS DENY DIFFERENT THINGS. The sentence below is about a WordPress site and a site administrator; on a hosted address the visitor has never seen either, so it would be a denial that misidentifies itself - which reads to them as the product being broken rather than the address being wrong. */ if (isHostedMount()) { return ( No practice is registered at this web address. Check the address for a typo, or ask the practice for the link to their portal. ); } return ( This WordPress site has not been configured with an organization yet. Please contact the site administrator to set up the organization before you can use this application. ); } /* * - THIS GATE TOOK EVERY PATIENT PORTAL DOWN, and the change that did it was correct. * * It used to read the `baa-status` extension directly. then narrowed the UNAUTHENTICATED * Organization read to an allow-list - rightly: that response was handing a practice's Stripe * subscription and BAA status to anyone who could read its homepage - and `baa-status` was not on it. * the field could no longer ARRIVE, `baaIsActive` was false for every organization always, and every * signed-in patient at every practice got the refusal below and nothing else. * Nothing was misconfigured: an extension being hidden by default is the property that allow-list * exists to create. What no server-side check could see is that a CLIENT GATE was consuming one *. * Spencer ruled the shape (2026-08-16, option B): the server DERIVES a narrow public boolean from * the BAA and publishes that; the BAA status itself stays private. So this reads the derived flag. * Do not "simplify" this back to reading `baa-status` - it is deliberately not public, and the read * would silently be false for everyone again. * And do not delete the gate because it is awkward: a practice that has not executed a BAA must * still be refused. That is a compliance boundary, not a nicety. * * `=== false` rather than `!flag`, and the difference is the whole failure mode above. The server * emits this for every organization, true or false - but a plugin build newer than the api deploy * would see it ABSENT, and treating absent as "no portal" is exactly the outage this card is. Absent * means NOT KNOWN, and not-known keeps the portal open; only a positive `false` closes it. */ // A hosted door carries no wpData, so its context comes from the path; its patient door is gated too. const portalContext = context ?? (isHostedMount() ? hostedMountContext() : undefined); if (portalContext === 'frontend' && patientPortalEnabled(wpOrganization.current) === false) { // The same card the practice's sign-in wears, so the patient still sees whose door this is. return ( This practice is not taking patient sign-ups or sign-ins online yet. Please contact the practice directly. ); } return ( {isWpAdminMount() && adoptedCode && ( )} {children} ); }; /** * How long to wait for an identity before admitting it is not coming. * * Generous rather than tight: the normal path resolves in well under a second, and the cost of being * wrong in each direction is not symmetric. Too short interrupts a slow-but-working sign-in with an * alarming screen; too long only means somebody already stuck waits a little more. */ const IDENTITY_STALL_MS = 15000; /** * True once `waiting` has been continuously true for long enough to stop calling it "loading". * * Shared by every gate below that would otherwise render a spinner with no end. Each one still owns * its own SENTENCE, because "we could not open your account" and "you have no workspace here" are * different facts and only one of them is recoverable by signing in again. */ const useStalled = (waiting: boolean): boolean => { const [stalled, setStalled] = useState(false); useEffect(() => { if (!waiting) { setStalled(false); return; } const timer = setTimeout(() => setStalled(true), IDENTITY_STALL_MS); return () => clearTimeout(timer); }, [waiting]); return stalled; }; /** * A SPINNER MUST NOT BE THE LAST THING A SCREEN DOES. * * `isLoggedIn` is `!!localStorage[accessToken]` - purely local, and true the instant a token exists. * `currentUser` is the identity RESOLVED over the network, and that resolution has several ways to * end without producing one: a session belonging to the other portal, an identity kind with no FHIR * resource type, a failed fetch. So `isLoggedIn && !currentUser` is not a transient loading state at * all - it is the resting state of every identity resolution that did not finish, and it was rendered * as an indefinite spinner with no message and no way out. * * Walked 2026-08-26: signed in at the patient portal, opened the staff portal, and got a spinner that * was still spinning two minutes later while every network call had returned 200. A spinner reads as * "wait", so the person waits instead of doing the one thing that would recover it. * * This does NOT diagnose why the identity failed to resolve, and is not meant to. It converts an * indefinite hang into a statement and an exit; the underlying cause is tracked separately. */ const SessionCouldNotResolve = ({ onSignInAgain }: { onSignInAgain: () => void }) => (

We could not open your account

We could not load your account for this portal. Sign in again here.

); const RequireAuth = () => { const { isLoggedIn, isLoading, currentUser, currentOrganization, logout, userRole, USER_ROLES, joinSetup, recheckJoinAccess } = useAuthServiceContext(); // The bounded wait right after joining a practice: it ends on its own, so no stall screen may fire inside it. const settingUp = joinSetup === "setting-up"; // Before the early returns, not beside its use below - this component returns in three places and a // hook called after any of them is a rules-of-hooks violation that only shows up as a crash on the // render where the branch flips. const isPatientViewer = useIsPatientViewer(); const closedToStaff = useClosedToStaff(isPatientViewer ? undefined : currentOrganization); const tenant = useTenant(); const { mayCall } = useCapabilityServiceContext(); const awaitingIdentity = isLoggedIn && !isLoading && !currentUser; const identityStalled = useStalled(awaitingIdentity && !settingUp); // A signed-in session whose identity never resolves used to wait here forever. Every request can // succeed, no error can be thrown, and the page still never finishes - so a spinner is not a safe // last state for this branch. Above the return below: under it, the render where that return first // fired ran one hook short and crashed every route with React #300. const resolving = isLoading || (isLoggedIn && !currentUser); const stalled = useHasStalled(resolving && !settingUp); const gate = sessionGateFor({ settingUp, resolving, awaitingIdentity, identityStalled, sessionStalled: stalled }); if (gate === "setting-up") { return ; } if (gate === "could-not-resolve") { return ; } if (gate === "stalled") { return ; } if (gate === "loading") { return ; } if (!isLoggedIn) { return ; } // §1b - THE FRAME KEYS OFF AUTHENTICATION STATE, and this component IS that boundary, so // the frame header mounts here rather than in a layout of its own. Logged out you never reach this // branch: you are redirected to the login page, which is a public page of the practice's website // and keeps THEIR header and menu. That is what makes signing out hand the visitor back to the // site rather than into a room with no doors. // // IT MUST BE INSIDE THE GATE, NOT ABOVE IT. Rendering it around the redirect would brand the // login page as the app - the exact thing the auth-state rule was chosen over "is this a portal // page" to prevent. // // == - THE HEADER AND THE APP NOW SHARE ONE VIEWPORT INSTEAD OF EACH CLAIMING A WHOLE ONE == // // These two were siblings in a bare fragment, and the app shell below asked for `min-h-screen` // (100vh) while sitting UNDER this ~46px header. So the pair was always 100vh + header, and EVERY // signed-in page overflowed by exactly the header's height. No content, data volume or screen size // could ever make that fit. // // A flex column with the shell flexing is the fix rather than `calc(100vh - 46px)`, because it needs // to know NOTHING about how tall the header is. The header has no fixed height - it is `py-3` around // a 30px logo - so a hardcoded number would go stale the first time it changed, and silently: the // symptom is 46px of scroll, which reads as ordinary page length rather than as a bug. // The header's Search, Book and Register follow the caller's grants, in the desk and clinic workspaces only. const deskGrants = frontDeskActionGrants({ isPatientViewer, closedToStaff, isReceptionist: userRole === USER_ROLES.RECEPTIONIST, deskWorkspace: userRole === USER_ROLES.RECEPTIONIST || userRole === USER_ROLES.PRACTITIONER, mayCall, noAccess: Boolean(tenant.noAccess), }); const frame = ( /* The hook the admin-bar offset selects, and it exists because there was nothing stable to select. A practice owner signed in to their own WordPress site gets the admin bar fixed across the top of every page - including this one - and it sat over the portal's own header, so the navigation button could not be clicked at all. The browser named an `ab-item` link as the topmost element at the button's centre. The bar is NOT hidden. It is WordPress's own, on a site the practice owns; suppressing it on their pages is a surprise we have no business springing. The frame moves instead, in index.css. */
{/* Card 305 R1 — the header is ONE BAND: brand and contact on the left, the patient's standing utilities on the right. Both slots render nothing for a staff viewer, so this stays one header for every role rather than becoming two. The hamburger is NOT here — it lives beside the breadcrumb in DashboardPage, where the drawer's own state is. Moving it would mean lifting that state into a context, which is a change to work that landed today; it is reachable where it is. Noted on the card. */} {/* CARD 683 — THE SUPPORT TAB HANGS OFF THIS HEADER, so the widget lives beside it rather than in DashboardPage. Spencer: *"a small tab attached to the top right (right under the bottom right of the header) as basically part of the header element."* `relative` here is what `top-full` inside the widget resolves against, and it is why no pixel height is written down anywhere — the comment above is explicit that this header has no fixed height and that hardcoding one goes stale silently. MOVED RATHER THAN DUPLICATED: it used to mount inside DashboardPage, 64px down the viewport and aligned to nothing. Mounting it here widens WHERE it renders — every route under this frame, not only the dashboard — which is the point: *"I want the llm assistant on all workspaces."* This frame is already auth-gated, so it still never renders signed out. THE PROVIDERS IT NEEDS, and this is worth stating because a context hook shipped without its provider takes the ENTIRE chrome down while every local test stays green. The assistant reaches the server through its own service, but the setup nudge beside it reads the auth, practitioner and schedule contexts to work out what the ASKER has left unfinished. All three are above this frame - it is a route element inside AppServiceProviders and FhirServiceProviders - checked before wiring it. Anything added to that panel must be checked the same way. */}
{/* CARD 722 — the settled two-line header. `contactRow` is the WHOLE second line, and it is `undefined` rather than an element that renders nothing, because React cannot see that a child returned null: passing one would make every staff frame reserve a row's worth of gap forever, for content that is never there. The role test is asked HERE, once, and the frame stays one component for both deployments and every role. */} : undefined} identity={} actions={ deskGrants.search || deskGrants.register || deskGrants.book ? : undefined } /* The practice's address, drawn under its name in the brand block. Patient-only for the same reason the second row is: a practice does not need its own street address read back to it on every screen its staff open. */ practiceAddress={ isPatientViewer && currentOrganization?.address?.[0] ? formatSingleAddress(currentOrganization.address[0]) : undefined } />
{/* In the frame rather than a page, so every signed-in screen carries it, the workspace chooser included. The gate holds both until the API has said live or sandbox (admin-v2 sandbox-unavailable). */} {joinSetup === "unconfirmed" && !isPatientViewer && ( )} {/* A page opened for one viewer, acting-as subject or practice starts over for the next. */}
); // ONE BOOKING SURFACE, AND THE FRAME IS THE ONLY PLACE IT CAN LIVE. The header's Schedule // appointment used to navigate to a `book-appointment` PAGE while the appointments page opened a // dialog holding the same component - two surfaces, and the outdated one was the one the header // used. Spencer retired the page. The header sits here and the home's next-appointment card is // several routes down, so a dialog mounted in either is unreachable from the other; hosting it at // the frame is what stops the fix from adding a third. // // PATIENTS ONLY. Staff have their own booking screen and their frame renders the same chrome, so // wrapping unconditionally would mount a patient dialog behind every staff page. The hook the // openers use is null-safe for exactly this reason - no provider means the opener does nothing. // TWO FRAME-LEVEL SHEETS, ONE HOSTING MECHANISM. The switcher needs to be reachable from every // view for exactly the reason the booking dialog does - the header is outside all of them - so it // nests here rather than inventing a second way to do the same thing. The switcher is OUTSIDE the // booking provider because the header it serves is inside the frame either way, and the order // makes no difference to what either can reach. // The inbox provider owns the patient's one unread count: header, nav and Inbox tab all read it. // Outermost, so the dialogs these providers host sheet on a phone as the frame's own do. return isPatientViewer ? ( {frame} ) : ( {frame} ); }; const RequirePractitioner = () => { const { currentUser, USER_ROLES } = useAuthServiceContext(); const isPractitioner = currentUser?.fhir?.resourceType === USER_ROLES.PRACTITIONER; return isPractitioner ? : ; }; /** * THE SAME HANG, ONE LEVEL DOWN. These gates wait on `activeWorkspace`, which is * `selectedWorkspace || userRole` - and `userRole` is only ever set when a workspace turned out to be * assignable: * * if (nextWorkspace) { setSelectedWorkspace(...); setUserRole(nextWorkspace); } <- no else * * So a staff account whose roles yield no openable workspace never gets a `userRole` at all, and every * gate below waits for something that is not coming. It is a reachable state rather than a theoretical * one: a clinician workspace is gated on the clinician seat, so a practitioner who has not been granted * one can resolve an identity perfectly and still open nothing. * * The exit is the profile, not "sign in again": the session is fine and signing in again would land in * exactly the same place. What that person needs is somebody at the practice to grant them a workspace, * so the screen says that and gives them the one page they can still reach. */ const WorkspaceUnavailable = () => { const { recheckOwnProfileOnce } = useAuthServiceContext() as { recheckOwnProfileOnce?: () => Promise }; const [rechecked, setRechecked] = useState(!recheckOwnProfileOnce); // A read that never answers ends the wait the same way every other wait in this file ends. const stalled = useStalled(!rechecked); // A member who has just joined has landed here while a reload showed their workspace, so the profile is // read once more before sending them to an administrator. useEffect(() => { if (!recheckOwnProfileOnce) return; let live = true; void recheckOwnProfileOnce().finally(() => { if (live) setRechecked(true); }); return () => { live = false; }; }, [recheckOwnProfileOnce]); if (!rechecked && !stalled) return ; return (

You do not have a workspace here yet

Your account is set up. Ask a practice administrator to give you access to a workspace.

Go to my profile
); }; // A patient or helper never gains a staff workspace, so a staff gate settles them at once instead of waiting. // The same identity test as RequirePatient; an identity not yet read keeps waiting. const useSettledPatientViewer = (): boolean => { const { currentUser, USER_ROLES } = useAuthServiceContext(); return [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(currentUser?.fhir?.resourceType as string); }; const RequireBuiltInStaffWorkspace = () => { const { USER_ROLES, availableWorkspaces } = useAuthServiceContext(); const { activeWorkspace } = useStaffPermissionServiceContext(); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(!activeWorkspace); if (!activeWorkspace && patientViewer) return ; if (!activeWorkspace && stalled) { return ; } if (!activeWorkspace) { return ; } const builtInWorkspaces: string[] = [ USER_ROLES.PRACTITIONER, USER_ROLES.RECEPTIONIST, USER_ROLES.ADMIN, USER_ROLES.PLATFORM_MANAGER, ]; // Said where they are, never a silent jump to Profile; nothing of the page mounts. return builtInWorkspaces.includes(activeWorkspace) ? ( ) : ( builtInWorkspaces.includes(workspace))} /> ); }; const RequireStaffWorkspace = () => { const { activeWorkspace } = useStaffPermissionServiceContext(); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(!activeWorkspace); if (!activeWorkspace && patientViewer) return ; if (!activeWorkspace && stalled) { return ; } if (!activeWorkspace) { return ; } return isStaffMessagingWorkspace(activeWorkspace) ? ( ) : ( ); }; /** * `orWorkspace` NAMES THE WORKSPACES THE SERVER ADMITS WITHOUT A CONFIGURED SECTION. * * A Practitioner's built-in level is `none` on every section, so a route gated on a section alone is * closed to every clinician the practice has not also configured as bench staff. The API does not * agree: `allowedRoles` admits PRACTITIONERS by master role and `requiredPermission` narrows only * Staff. A client stricter than its server refuses work the API would accept, which is the same * defect as offering work the API refuses, wearing the other face. */ const RequireStaffPermission = ({ section, manage = false, orWorkspace, }: { section: StaffPermissionSection; manage?: boolean; orWorkspace?: readonly string[]; }) => { const { canView, canManage, activeWorkspace, permissionsSettled } = useStaffPermissionServiceContext(); const authorized = Boolean(activeWorkspace) && ((manage ? canManage(section) : canView(section)) || Boolean(orWorkspace?.includes(activeWorkspace!))); // A reload restores the workspace before the practice's permissions arrive, so a denial waits for them. const waiting = !activeWorkspace || (!authorized && !permissionsSettled); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(waiting); if (!activeWorkspace && patientViewer) return ; // IT WAITS, LIKE ITS FOUR SIBLINGS. An absent workspace is not a wrong one, and this gate's verdict // is a replacing redirect that cannot be re-taken on the next render - so deciding before the // workspace resolves is permanent. The permission itself is resolved FROM the workspace, so this was // latent here before `orWorkspace` made it visible: an unresolved workspace reads as `none` on every // section and sends the caller to the no-route-grant fallback. if (waiting && stalled) { return ; } if (waiting) { return ; } // A tech whose role has no Lab and imaging work is told what is missing, not only that it is refused. if (!authorized && section === "diagnostics" && !canView(section) && workspaceWorksTheDiagnosticQueue(activeWorkspace)) { return ; } // Said where they are, never a silent jump to Profile: a typed address, or access withdrawn while open. return authorized ? : ; }; /** * - THE ONE GATE THAT DID NOT WAIT, and it is the extra hop behind the wrong landing. * * It read `activeWorkspace === PLATFORM_MANAGER`, so an UNRESOLVED workspace took the same branch as a * wrong one and fired ``. On a workspace SWITCH that redirect races the * switcher's own navigation to the new workspace's landing - and being a `replace`, whichever renders * last wins. * * THAT IS WHY THE DEFECT WAS DIRECTIONAL. Leaving Platform Manager was the only origin whose routes sat * behind a gate that redirects on an absent workspace; `RequireAdminWorkspace` waits, and Billing's * screens are permission-gated rather than workspace-gated. So Admin -> HR and Billing -> HR landed * correctly and Platform Manager -> HR did not, with nothing about HR being wrong. * * It now waits, like its three siblings. An absent workspace is not a wrong one. */ const RequirePlatformManager = () => { const { USER_ROLES } = useAuthServiceContext(); const { activeWorkspace } = useStaffPermissionServiceContext(); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(!activeWorkspace); if (!activeWorkspace && patientViewer) return ; if (!activeWorkspace && stalled) { return ; } if (!activeWorkspace) { return ; } return activeWorkspace === USER_ROLES.PLATFORM_MANAGER ? ( ) : ( ); }; const RequireAdminWorkspace = ({ redirectPath = "/dashboard/organization", }: { redirectPath?: string; }) => { const { USER_ROLES } = useAuthServiceContext(); const { activeWorkspace } = useStaffPermissionServiceContext(); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(!activeWorkspace); if (!activeWorkspace && patientViewer) return ; if (!activeWorkspace && stalled) { return ; } if (!activeWorkspace) { return ; } return activeWorkspace === USER_ROLES.ADMIN ? ( ) : ( ); }; // The BAA is the Owner's to sign whatever workspace they are in; the server's owner gate still decides every write. // Anyone else outside the Admin workspace keeps the redirect. const RequireBaaSheet = () => { const { USER_ROLES, currentUser, currentOrganization } = useAuthServiceContext(); const { activeWorkspace } = useStaffPermissionServiceContext(); const { fetchOwner } = useOrganizationOwnerService(); const patientViewer = useSettledPatientViewer(); const stalled = useStalled(!activeWorkspace); const isAdmin = activeWorkspace === USER_ROLES.ADMIN; const organizationId = currentOrganization?.id; const practitionerId = currentUser?.fhir?.resourceType === "Practitioner" ? currentUser.fhir.id : undefined; // Keyed to the practice and the person it was read for, so a verdict never outlives either. const ownerKey = organizationId && practitionerId ? organizationId + "|" + practitionerId : undefined; const [ownerCheck, setOwnerCheck] = useState<{ key: string; isOwner: boolean } | undefined>(undefined); const viewerIsOwner = ownerKey && ownerCheck?.key === ownerKey ? ownerCheck.isOwner : undefined; // Read in every workspace so a later switch decides in the same render and the switcher's landing still wins. useEffect(() => { if (!organizationId || !ownerKey) return; let active = true; fetchOwner(organizationId) .then((state) => active && setOwnerCheck({ key: ownerKey, isOwner: state.status === "accepted" && referencesPractitioner(state.ownerPractitionerId, practitionerId) })) .catch(() => active && setOwnerCheck({ key: ownerKey, isOwner: false })); return () => { active = false; }; }, [organizationId, practitionerId, ownerKey, fetchOwner]); // After every hook, so the hook count is the same whichever way identity resolves. if (!activeWorkspace && patientViewer) return ; if (!activeWorkspace && stalled) { return ; } if (!activeWorkspace) { return ; } if (isAdmin) return ; if (!ownerKey) return ; if (viewerIsOwner === undefined) return ; return viewerIsOwner ? : ; }; const RequirePatient = () => { const { currentUser, USER_ROLES } = useAuthServiceContext(); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes( currentUser?.fhir?.resourceType as string, ); return isPatient ? : ; }; // Messaging and collaboration teams are 18+. A signed-in 12-17 patient who types the address lands // home: the surfaces are absent, not explained (the nav filter hides the same two). const RequireAdultSelf = () => { const { currentUser } = useAuthServiceContext(); return isAdolescentSelf(currentUser) ? : ; }; // The practice front door. A signed-out visitor on a hosted PATIENT mount gets the landing page; // every other mount keeps the redirect it has always had, because a WordPress page is the practice own // site already and its root is not ours to take. const RootEntry = () => { const { isLoggedIn } = useAuthServiceContext(); // The org-less portal has no practice to land on; its front door is the sign-in. if (!isLoggedIn && isOrglessPatientsHost()) return ; if (!isLoggedIn && isHostedMount() && isPatientExperience()) return ; return ; }; const LoginRedirect = () => { const { isLoggedIn, wpOrganization } = useAuthServiceContext(); const location = useLocation(); const { fetchOrganizationUserCount } = useOrganizationServiceContext(); const [isCheckingUsers, setIsCheckingUsers] = useState(true); const [hasUsers, setHasUsers] = useState(false); // S4e-2 - capture the CTA attribution BEFORE any branch below, including the // already-logged-in Navigate: a patient who still has a session open when they click the CTA // must still have the click attributed to a booking made during this visit. // // Reads window.location.href directly rather than react-router's useSearchParams: this app is a // HashRouter, so the token the server's redirect appends lands INSIDE the hash fragment // (.../#/login?cta=). All the orchestration (extract early-return-if-absent stash // strip the address bar) lives in captureCampaignCtaAttribution (utils/campaignCtaAttribution.ts) // so it is unit-tested directly, independent of this effect merely existing - mirrors the existing // useProofingReturn.tsx/utils/proofingFlow.ts split for the identity-proofing return marker. Runs // once on mount: LoginRedirect is freshly mounted whenever this route is navigated to, so there is // no missed re-render to guard against. useEffect(() => { captureCampaignCtaAttribution(window.location.href); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const checkUsers = async () => { if (!wpOrganization.current || !isWpAdminMount()) { setIsCheckingUsers(false); return; } try { const total: { total: number } = await fetchOrganizationUserCount( wpOrganization.current as Organization, ); setHasUsers(total.total > 0); } catch (error: unknown) { console.error(error); setHasUsers(false); } finally { setIsCheckingUsers(false); } }; checkUsers(); }, [fetchOrganizationUserCount, wpOrganization]); if (isLoggedIn) { return ; } if (!isWpAdminMount()) { return ; } if (isCheckingUsers) { return ; } return hasUsers ? : ; }; const SignupRedirect = () => { const { isLoggedIn, wpOrganization } = useAuthServiceContext(); const { fetchOrganizationUserCount } = useOrganizationServiceContext(); const [isCheckingUsers, setIsCheckingUsers] = useState(true); const [hasUsers, setHasUsers] = useState(false); useEffect(() => { const checkUsers = async () => { if (!wpOrganization.current || !isWpAdminMount()) { setIsCheckingUsers(false); return; } try { const total: { total: number } = await fetchOrganizationUserCount( wpOrganization.current as Organization, ); setHasUsers(total.total > 0); } catch (error: unknown) { console.error(error); setHasUsers(false); } finally { setIsCheckingUsers(false); } }; checkUsers(); }, [fetchOrganizationUserCount, wpOrganization]); if (isLoggedIn) { return ; } if (isOrglessPatientsHost()) { return ; } if (isPatientExperience()) { return ; } // Staff never self-register (admin creates them) - redirect to login. // NOTE: intentionally NOT isStaffExperience() here - that helper is true for // 'admin' too, which would short-circuit the existing admin first-run signup // gating below. Only the new 'staff' mount should be redirected. if (getMountContext() === "staff") { return ; } if (isCheckingUsers) { return ; } return hasUsers ? : ; }; /** * - IT DECIDED WHERE TO SEND SOMEBODY BEFORE IT KNEW WHICH HAT THEY WERE WEARING. * * Every branch below keys on `activeWorkspace`. When it is not resolved yet, none of them matches and * the fall-through sends a staff member to `/dashboard/profile` - which is the code's NO-ROUTE-GRANT * fallback, and its own guard test calls it out by name as reading "as a broken product rather than a * permission". A `` is irreversible, so a decision taken one render too early is * permanent: the person lands on their own name-and-address form and cannot tell whether their new * workspace has no home screen, or they have lost access, or something failed. * * MEASURED SYMPTOM: switching from Platform Manager to HR landed on `/dashboard/profile`, twice, while * Billing -> HR and Admin -> HR landed correctly on `/dashboard/staff`. It reads as origin-dependent, * and the mechanism is the same either way - the origin only decides whether an extra redirect hop is * taken through here while the workspace is still settling. * * THE THREE SIBLING GUARDS IN THIS FILE ALREADY WAIT. `RequireStaffWorkspace` and * `RequireAdminWorkspace` both hold a spinner while `activeWorkspace` is absent and show * `WorkspaceUnavailable` once it has been absent too long. This was the one that did not, and it is the * only one of the four whose decision cannot be re-taken on the next render. */ /** * WHERE ASKING FOR NOTHING SENDS SOMEBODY - and it is the ordinary path, not an exotic one: the * dashboard's own index route is ``, so this decides the landing for everybody * who arrives at `/dashboard`. * * IT USED TO HOLD ITS OWN COPY OF THE WORKSPACE-TO-LANDING LIST, three workspaces short. * Measured 2026-09-08 by signing in as the imaging technician: Referrals, LabTech and ImagingTech * had no branch, fell through the tail below, and arrived at a form for editing their own name. * Nothing was unreachable and no guard refused anything - **one of two answers to one question * simply had three fewer answers**, and the two also disagreed about the Platform Manager's. * * The list is now `utils/staffLanding.ts` and this asks it. The TAIL is kept and is not the same * thing as the list: a signed-in Practitioner-typed account whose workspace resolves to nothing we * know still goes to Profile rather than to a patient's home page. */ const HomePageRedirect = () => { const { currentUser, availableWorkspaces, selectedWorkspace } = useAuthServiceContext(); const { activeWorkspace } = useStaffPermissionServiceContext(); /* * A PROVISIONAL WORKSPACE IS NOT A SETTLED ONE, and this gate could not tell them apart. * * `activeWorkspace` is `selectedWorkspace || userRole`. At sign-in `userRole` resolves FIRST, off * the Cognito group - and the permission-driven workspaces (Billing, HR, Referrals, LabTech, * ImagingTech) are not Cognito groups at all, so it holds something else entirely for a moment. * The `!activeWorkspace` guard below waits for an ABSENT workspace and this one is not absent; it * is wrong, briefly, and `Navigate ... replace` is final. * * MEASURED 2026-09-08 by recording every hash a sign-in passes through: * * #/login -> #/dashboard/home -> #/dashboard/profile * * and asking for #/dashboard by hand a minute later, once everything had settled, sent the same * imaging technician to #/dashboard/diagnostic-orders through this same component. Same route, * same account, two answers - which is what says it is timing and not the list. * * SO IT WAITS FOR THE CHOICE, not merely for a value. Somebody who HAS workspaces but has not * been given one yet is mid-resolution; somebody with none never will be, and falls through to * the guards below exactly as before. Same shape as the three sibling gates in this file, whose * own note reads: an absent workspace is not a wrong one. */ const settling = availableWorkspaces.length > 0 && !selectedWorkspace; const stalled = useStalled(!activeWorkspace || settling); // Says what is true and what to do, rather than dropping them somewhere that looks like a destination. if ((!activeWorkspace || settling) && stalled) { return ; } if (!activeWorkspace || settling) { return ; } const landing = defaultDashboardPathFor(activeWorkspace); // NO LANDING FOR THIS WORKSPACE IS NOT THE SAME ANSWER AS PROFILE, which is why the resolver says // `undefined` rather than deciding for both callers. A staff-mounted Practitioner goes to Profile; // anybody else - somebody carrying a stale stored workspace on the patient mount, say - keeps the // patient home they had before. if (!landing) { return isStaffExperience() && currentUser?.fhir?.resourceType === "Practitioner" ? : ; } return ; }; // const OrganizationRedirect = () => { // const { userRole, USER_ROLES, wpOrganization } = useAuthServiceContext(); // if ( // userRole && // [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole as string) // ) { // return ( // // // // // // ); // } // return ; // }; const AppServiceProviders: React.FC = ({ children }) => { return ( {/* Directly under AuthServiceProvider: useHttpService, which TenantServiceProvider reads, needs useAuthServiceContext. Wraps everything else so any screen can render the sandbox banner. */} {/* AkashicAgreementsServiceProvider needs AuthServiceProvider above it (useHttpService reads useAuthServiceContext) and must sit above the gate component mounted in App.tsx below — see that import's comment. */} {/* Card 224 / B30 — the capability signal. Directly under AuthServiceProvider because it reads `currentUser` to know when to ask (and to not ask while signed out), and above everything that might gate a mount on it. It is deliberately safe to be missing: components fall back to the rule they used before it existed, so a failure here degrades to yesterday's behaviour rather than to a blank screen. */} {children} ); }; const FoundationServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const BaseServiceProviders: React.FC = ({ children, }) => { return ( {/* Card 263 E3 — note autosave. OUTSIDE ScribeServiceProvider in meaning even though it nests inside it here: writing a note is not the paid add-on, and nothing in this provider touches the scribe. */} {/* The PRIVATE note's autosave, and it sits beside the clinical note's for the reason card 1035 exists: the two tabs behaving oppositely is the defect, not the missing feature. */} {/* What a saved note offers to put on the record. It sits inside the note providers because that is the only surface that asks, and the editor reads it through a TOLERANT accessor - a suggestion feature must never take note writing down with it. */} {/* Beside the proposals and for the same reason: this is what the clinician adds most often, read through the same tolerant accessor so its absence costs a shortcut list and nothing else. */} {children} ); }; const SummaryServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const DiagnosticsServiceProviders: React.FC = ({ children, }) => { return ( {/* CARD 486's MISSING HALF. `QuestionnairesPage` consumes `usePublicFormServiceContext`, and this provider was defined but mounted NOWHERE — `PublicFormServiceProvider` appeared only inside its own definition file, never in this tree, on any branch. So the page threw on every render: "usePublicFormServiceContext must be used within a PublicFormServiceProvider". That is not a broken screen among several — the error boundary replaces the whole chrome INCLUDING the sidebar, so there is no nav to leave by, Dismiss does not recover, and `#/dashboard` itself crashes the same way. A PLATFORM MANAGER LANDS on this route, so that entire workspace was unreachable; every other role hits it the moment they open Forms/Questionnaires. It belongs HERE, inside QuestionnaireServiceProvider, for two reasons rather than convenience: the page renders public forms and clinical questionnaires as two sections of one screen, so anything holding the questionnaire context needs this one too; and this service's only dependency is `useHttpService`, which is already available at this depth — the sibling above it proves that. */} {children} ); }; const MedicationsServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const CareProvisionServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const RequestResponseServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const ClinicalServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const FhirServiceProviders: React.FC = ({ children, }) => { return ( {children} ); }; const PatientProfilePage = () => { const { id } = useParams(); return id ? : ; }; // Org-internal notes section (staff chart nav, Task 11). patientState is loaded by the parent // PatientPage before the Outlet renders this route, same source PatientTasks/PatientAppointments // already read from context - no separate fetch here. // Notes are staff-only: this route lives in the shared renderPatientRoutes() (reused by the // patient-self route too), so we hard-gate it to staff here - matching the staff-only nav link // and the server authz that denies /R4/Basic to patients/related-persons. Defense-in-depth: a // patient/related-person reaching this path directly renders nothing rather than a dead form. const PatientNotesSection = () => { const { patientState } = usePatientServiceContext(); const { currentUser, USER_ROLES } = useAuthServiceContext(); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes( currentUser?.fhir?.resourceType as string, ); if (isPatient || !patientState?.id) return null; return ; }; // - the patient-scoped access log. STAFF-ONLY, gated here for exactly the reason // PatientNotesSection above is: this route lives in the shared renderPatientRoutes(), which the // patient-SELF portal also mounts, and the server route admits practitioners/receptionists/admins only. // A patient or related person reaching this path directly would otherwise render a component whose one // request is guaranteed to 403 - they have their own cross-org feed at My History. const PatientAccessLogSection = () => { const { patientState } = usePatientServiceContext(); const { currentUser, USER_ROLES } = useAuthServiceContext(); const isPatient = [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes( currentUser?.fhir?.resourceType as string, ); if (isPatient || !patientState?.id) return null; return ; }; // Care-view anchor list section (Capability B2, Task 9). AnchorList reads the patient from // context - same pattern as PatientNotesSection above - and is read-only for staff + patient-self, // so no role gate here (unlike PatientNotesSection, which is staff-only). const AnchorListSection = () => { const { patientState } = usePatientServiceContext(); if (!patientState?.id) return null; return ; }; // Care-view anchor detail section (Capability B2, Task 9). Reads:anchorId from the route params. const AnchorViewSection = () => { const { anchorId } = useParams(); return anchorId ? : null; }; // Shared between the patient-SELF portal (under RequirePatient) and the STAFF "view a patient" // mounts (under RequirePractitioner, patients/:id). `patientPay` is passed true ONLY by the // self mount so the portal "Pay now" button + return poll never appear in the staff view. // `selfView` marks the PATIENT'S OWN mount (/dashboard/patient/*) as opposed to the two STAFF mounts // (/dashboard/patients/:id/*). It exists because a couple of these routes belong under the staff // patient-record shell but not under the patient's own: My Invoices is billing rather than a clinical // record and now has its own standalone page (see MyInvoicesPage), so keeping it here too would leave // two live URLs for the same surface, one of them wearing the wrong "My Records" header. const renderPatientRoutes = ({ patientPay = false, selfView = false }: { patientPay?: boolean; selfView?: boolean } = {}) => { return ( <> {/* Card 276 — on the PATIENT'S OWN chart the index is the record itself, not a redirect into one leaf of it. This is what the patient home's "My visits & results" door opens onto; sent to `profile` it would open onto a screen with no way onward, which is what made the door a dead end. The STAFF mounts keep the redirect: a clinician opening a chart wants the patient in front of them, and they already have `PatientChartSidebar` to move around. */} {/* CARD 747 — the approved record replaces the visit-first grouping that stood here. Same door, different organising rule: that page nested results UNDER the visit that produced them, which is only right for the results a visit produced. A prescription written between visits, or a lab a patient walked in for, had nowhere to go. One merged stream of events, newest first, and the visit becomes a container you OPEN rather than a bucket everything else is sorted into. */} {/* AN EVENT IS A CONTAINER, so the visit rows in the stream have to open onto something. THE CONTAINERS ARE CHILDREN OF THE STREAM, NOT SIBLINGS OF IT, and that one word is the whole fix. As siblings they rendered into the same outlet, so a detail match REMOVED `RecordStream` from the tree — and every filter chip, the search term and the scroll position lived inside the component that went away. A patient filtered to a condition and a year, searched inside it, opened one result and pressed back, and landed on the unfiltered top of the stream. Nested, the stream stays mounted and renders the detail as an OVERLAY through its own outlet. The state survives because nothing destroys it: there is nothing to serialise into the URL and no restore path to keep correct forever. The address is unchanged, deliberately. Opening from the list pushes it and never leaves the stream; arriving at it cold mounts the stream and opens the overlay on top. selfView ONLY, unchanged: a clinician opening a chart already has the encounter screen, and mounting a patient-facing container under the staff chart would change the staff chart as a side effect — which card 747 explicitly forbids. */} : }> {selfView && } />} {/* The other container. The stream's "Blood test · 4 results" row promises the four are one tap away, and it emits this path. */} {selfView && } />} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> }> {/* The approved medications page is the destination itself, not a redirect into one of the two resource lists it merges. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> }> {/* The approved page is the destination, not a redirect into the care-plans list. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Beside referrals, on the shelf the other request-family resources already share. A practitioner orders from a visit AND between visits, and this is the only surface that covers both -- the encounter screen has no ordering area at all. */} } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> {/* Adding and editing open as an overlay over the list; "new" arrives as the coverageId. */} } /> {/* Read-only — canManage=false hides New Invoice / status actions (Task 9's patient portal). patientPay enables the portal-only "Pay now" button + return-from-redirect poll (Slice B); the staff view never gets it (Slice C2). STAFF MOUNTS ONLY: the patient's own invoices moved to the standalone /dashboard/my-invoices (MyInvoicesPage), because this shell prints a "My Records" header that is wrong for billing. */} {!selfView && ( } /> )} {/* STAFF MOUNTS ONLY. Point-in-time reconstruction ("View as of…") and care-view anchors are clinical/evidentiary tooling — an anchor exists to prove what a clinician was looking at when they made a decision — so they are not part of the patient portal. Removed from the patient-self mount along with their entry points (PatientPage's header buttons and EncounterPage's "View care-view anchor" action), so no patient-reachable link is left pointing at a route that no longer exists. */} {/* The staff Record states a refusal where the patient's record cannot be read. */} {!selfView && } />} {!selfView && } />} {!selfView && } />} {!selfView && } />} ); }; // D4, Task 12 (Spencer's ruling, 2026-08-04): "account recovery is a floor, not billing // gated." OrganizationCheck runs three checks before any route renders - BAA-inactive, a // suspended/canceled subscription, and a transient org-fetch failure that calls logout() - and for // every other route that's correct: those three states mean "the portal is down." For these two // routes it means something categorically worse. /credential-unlock is the ONLY door back into an // account the patient locked themselves after reporting it compromised, with NO staff override by // design; /credential-revert is the "wasn't me" link that puts them in that locked state in the // first place. A practice's declined card, a lapsed BAA, or one org-lookup blip must never be able // to strand a patient outside their own medical record for a reason they cannot influence. // // A SIBLING BLOCK INSIDE OrganizationCheck IS NOT ENOUGH - OrganizationCheck would still // run and could blank the screen or call logout() before the sibling ever gets a chance to render. // This has to short-circuit BEFORE OrganizationCheck mounts at all. (It used to say "mirroring the // getMountContext() === 'public' branch below" - that branch was deleted with, because the // public form is a cross-origin frame now and must never render inside a practice's own document.) // Do not "fix" this by moving the two routes back inside // OrganizationCheck's children - see // src/pages/CredentialUnlockPage/__tests__/credentialRoutesArePublic.test.ts, which pins both this // and the pre-existing RequireAuth exemption, and reds if either is undone. // // Both pages are wrapped in their own ErrorBoundary (harmless here - ErrorBoundary holds no context // of its own): /credential-unlock is the one page on this entire site where an uncaught render error // must not mean a blank screen, since there is no other way in. const CREDENTIAL_RECOVERY_PATHS = ['/credential-revert', '/credential-unlock', '/recover-by-phone']; const isCredentialRecoveryPath = (pathname: string): boolean => CREDENTIAL_RECOVERY_PATHS.includes(pathname); const AppRoutesGate = () => { const { pathname } = useLocation(); const { crossPortalConflict, agencyTermsOwed } = useAuthServiceContext(); if (isCredentialRecoveryPath(pathname)) { return ( } /> } /> } /> ); } // An emailed invoice pay link is opened by whoever holds it, signed in or not, and never as anyone's session. if (pathname === PAY_LINK_PATH) { return ( } /> ); } /** * - asked BEFORE the session is taken, and therefore ahead of everything else. * * It sits OUTSIDE `OrganizationCheck` deliberately. A staff session restored on the patient mount * has not resolved an identity, so the org checks below it either cannot run or would run for the * wrong audience - and any of them reaching `logout()` first would destroy the very session this * exists to protect. The question has to come before anything that might answer it by accident. */ if (crossPortalConflict) { return ; } // Same placement and reason as the gate above: the session is still live, and nothing below may end it. if (agencyTermsOwed) { return ; } return ( <> {/* The one way out that is reachable everywhere this gate is - OrganizationCheck below renders nothing until a session resolves, so /login, the resolving spinner and every early return it can take all sit under this instead of inside the frame. Self-gating: renders nothing outside Sandbox Mode, and steps aside once the API confirms the sandbox and the banner's own Leave takes over. */} {/* Card 288 — inside OrganizationCheck so it only runs for a resolved, signed-in session, and OUTSIDE the routes so navigating does not remount the clock and reset the hour. */} {/* Public routes */} } /> } /> } /> {/* Card 073 S1b — the emailed care-team invitation link. PUBLIC on purpose and it MUST stay outside RequireAuth: the invitee has no Akashic account yet (accepting is what creates one), so gating this on a session makes the invitation unusable. The endpoint is Authorizer: NONE for the same reason — the emailed token is the only thing that can authenticate them. This is where EXTERNAL_INVITE_ACCEPT_URL must point. */} } /> {/* A practice's clinician invitation. Public so the link survives a signed-out arrival: it keeps the token for this session and goes through sign-in, and the dashboard asks once the person is signed in. */} } /> {/* The owner email's one Accept button. Public: the owner may have no account yet, and the link token is the only key. Opening it claims nothing; the press does. */} } /> }> {/* DashboardPage redirect */} } /> {/* DashboardPage */} }> } /> } /> {/* The owner accepts the MSA here, outside the Organization page an owner may not be able to open. */} } />}> } /> {/* Organization */} }> }> } /> } /> }> } /> } /> } /> }> } /> {/* What the practice runs and the tests it offers. `organization: manage` rather than `diagnostics`: that section governs the WORK, and curating the catalogue is configuration, beside locations and services. */} }> } /> } /> } /> } /> } /> } /> {/* Profile routes */} } /> } /> {/* Card 077 S4d Phase 2 — gated on the `campaigns` section, exactly as billing is on `billing`. A practice with campaigns:none never sees the route OR the nav entry, and the server refuses independently — the client gate is convenience, never the boundary. */} }> }> } /> } /> {/* Both served by ONE component: /new starts empty, /:id loads. `new` is declared FIRST so it is not captured by the `:id` param. */} } /> } /> }> } /> } /> } /> } /> {/* Card 319 scope item 3 — payments setup moved here from Platform Manager. The practice's Stripe connection decides whether a patient can pay a bill, so it belongs beside the bills. */} } /> } /> {/* Card 608 -- the referral coordinator's queue. NOT behind RequireStaffPermission. The coordinator ROLE carries no permission profile by design (card 605): the workspace is workflow, and access stays exactly where it was. Gating this on a permission section would ask for reach the role was deliberately never given, and the screen would 403 for the only people it is for. What it shows is already fenced server-side: the queue route forces the caller's own organization tag and returns a projection, not the referrals themselves. */} } /> {/* Records requests: `view` to read, as the API gates it; writes check manage on the page. */} }> } /> {/* Connections to other systems. The return route is where the server's SMART callback sends the browser with a one-time claim; it is registered per practice host, so its path does not move. */} } /> } /> {/* The lab queue -- every order this practice is working, across patients. BEHIND RequireStaffPermission, and this is where it differs from the referral queue above. That one is deliberately ungated because the coordinator role carries no permission profile by design. Lab work DOES have one: the `diagnostics` section exists so a practice can employ a lab tech who reaches the queue and nothing else, and the server gates the route on `view`. Leaving the client open would render a screen that then 403s -- the shape card 124 produced when a section the UI offered was refused by the API. */} }> {/* The bench sits beside the queue rather than replacing it: one is worked by specimen and one by order, and the epic keeps both. Same `view` gate, and for the same reason -- somebody can be shown the bench before the practice trusts them to accession against a patient. */} } /> } /> {/* `view`, like the queue beside it: a tech the practice has not yet trusted to write to a record can still see that a fax arrived, which is how somebody knows to fetch the person who can. The page offers typing one in and matching only on `manage`, which is the split the API makes. */} } /> {/* Laboratory results that came back electronically and wait for a person to match them. The same `view` gate; matching and setting aside are offered only on `manage`. */} } /> {/* READING IS A SECTION OF THE PRACTITIONER WORKSPACE, NOT A HAT, and it is reached on the `diagnostics` permission like the queue beside it (roles/diagnostics/lab-and-imaging.md section 11). The API refuses reporting, release and addenda without that permission, whatever the master role. */} }> } /> {/* Recording a result needs `manage`, not `view`. The queue's own note says a lab tech may watch it in their first week before the practice trusts them to write to a patient's record, and a screen that rendered on `view` would then 403 on submit -- offering work the API refuses. A practitioner who reads studies needs the same permission; the API does not admit them on master role. */} }> } /> {/* Booking now opens over the worklist; an old link lands there. */} } /> {/* CARD 636 — the practice's own fax account. Beside the referral workspace it serves rather than under Billing: faxing is how a referral leaves the building, and the coordinator who hits "this practice has not set up faxing" is the one who needs to find it. IT WAS REGISTERED INSIDE `RequirePatient` AND WAS THEREFORE UNREACHABLE BY THE ONLY PEOPLE IT IS FOR. Found in a browser on 2026-08-23: clicking "Faxing" in the Referrals workspace navigated to the PROFILE page — the guard did not match, so the path fell through to the dashboard's default child. It failed as a WRONG PAGE rather than as an error, which is why nothing reported it: the sidebar still rendered, the workspace was still Referrals, and only the content was somebody else's screen. Its neighbour `care-teams` legitimately IS a patient route (the patient portal's Collaboration Teams, hidden from a minor by nav id) — only this one was in the wrong group. */} } /> {/* Support - a practice raising a ticket from the assistant's escalation form (SetupHelpWidget) and following it afterwards. NOT behind RequireStaffPermission, the same reasoning as referrals and fax-settings just above: reporting that the product is broken is not itself a permissioned capability, and gating this on a section would refuse the screen to the very role most likely to need it - whoever hit the broken thing. The route is the cross-repo contract the emailed "ticket answered" link resolves to (see organizationPortalUrl.ts's fragment convention in akashic-infrastructure); it must not move without updating that builder to match. */} } /> } /> } /> } /> {/* Patient routes */} }> } /> {/* Patient Data — the patient-SELF portal: pass patientPay so the invoices page shows the "Pay now" button + runs the return-from-redirect poll. */} }> {renderPatientRoutes({ patientPay: true, selfView: true })} {/* Billing, not a clinical record — and its own top-level sidebar item. Mounted here rather than inside renderPatientRoutes for the same reason my-history is: that helper is shared with the STAFF patient view, and this is the patient's own surface. Under the shared shell it rendered beneath a "My Records" header. */} } /> {/* Card 059 — My History: who accessed and who changed the patient's data. Mounted here under RequirePatient rather than inside renderPatientRoutes, because that helper is shared with the STAFF patient view and this feed is the patient's own awareness tool, not a staff surface. */} } /> {/* A patient's own connections to other health systems. The return route is where the server's SMART callback sends a patient back on their practice's portal; its path does not move. */} } /> } /> {/* A patient's own support screen. Its own pair of routes rather than a flag on `support` above: the server keys a patient's ticket outside every practice partition, and one route serving both audiences is where they would meet. Inside the patient guard, so a practitioner never lands here. */} } /> } /> } /> } /> {/* RETIRED, AND REDIRECTED RATHER THAN DELETED. The booking PAGE was a second, outdated copy of the dialog and Spencer retired it; the dialog now hosts at the frame and opens from anywhere. A bookmarked URL lands on the patient's appointments instead of a stranded form - the list is where the thing they were trying to reach actually is, and it carries a Book Appointment button. */} } /> }> } /> } /> } /> } /> {/* Not a page. A practice opens inside its card in the list above; this keeps an old link working and carries its id across. */} } /> {/* Spencer, 2026-08-13: "care-teams should be collaboration-teams unless you're looking at a true FHIR care team". This is OURS. The clinical CareTeam keeps its name under care-provision. */} }> } /> {/* A team is an overlay over the list, not a page of its own. */} } /> {/* Staff routes */} }> }> }> } /> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> {/* Another workspace is told in place, before the list mounts, never sent to Profile. */} } /> {/* Card 329 — bringing an existing practice's patients in. Declared BEFORE `patients/:id` so the literal segment is not read as a patient id. */} } /> } /> {/* Patient Data */} {/* A desk-only caller at a chart address gets the desk's patient view (DeskChartGate). */} {/* Inside the desk gate, so the desk view stays; any other refused workspace is told before the chart mounts. */} }> {renderPatientRoutes()} } > {renderPatientRoutes()} } /> } /> } /> } /> } /> } /> {/* Card 311 — the add-ons page is ABSORBED, not moved. The settled design has no second screen: the subscription page IS the upgrade view, so every row shows what you have and is the control for changing it. The ROUTE survives as a redirect rather than being deleted. It is a live URL that has been linked from the suspended-plan alert and could sit in a bookmark or an email; a 404 there lands an admin who is trying to PAY US on a dead end. */} } /> } /> } /> } /> } /> } /> } /> {/* Staff messaging is available to all six staff workspaces (incl. Billing + HR), not just the built-in four. */} }> } /> }> } /> }> } /> {staffRecordRoutes(RequireStaffPermission)} }> {/* Creating staff is an onboarding duty — delegated staff:manage, not admin-only. Admin-workspace granting on the new-staff form is guarded inside NewUserPage. */} } /> )} > {/* The permission MATRIX (what each workspace may do) stays admin-only. */} } /> } /> } /> } /> } /> {/* Card 259 — this node's POSITION is why the agreement gate portalled to : it is rendered at the end of the tree, inside `OrganizationCheck`, so a dialog that opens on first paint looked for it before it existed. The real fix is in `getAkashicPortalContainer`, which now creates the node when it is missing — so this can no longer be a race whatever the render order becomes. It stays here because `CallOverlay` lives inside it and needs the provider tree above; the helper makes its timing stop mattering rather than the position being load-bearing. */}
); }; function App() { // - THE IN-PAGE PUBLIC-FORM BRANCH IS GONE, AND ITS REMOVAL IS THE POINT. // // A standalone `getMountContext() === 'public'` branch used to render `` here: the // anonymous form, in the practice's OWN document, beside every other script on their site. // replaced that with a cross-origin iframe and deleted the shortcode path that produced the mount. // // The BRANCH survived that deletion, and a branch is a door. No PHP in this plugin emits // `data-akashic-mount="public"` any more - so nothing we serve can reach it - but any page still // carrying the old markup (a page builder that stored rendered output, a cache, a hand-pasted probe) // re-mounted the in-page form on the current build and put the fields back in the practice's document. // That is not hypothetical: a leftover page on the second local WP install was still rendering the // in-page shell on 2026-08-21, against the current bundle. // Deleted rather than guarded: the frame is the only embed, so there is no second path to keep. // A connection return's claim and attempt leave the address on the first render, before any sign-in guard. useState(() => captureConnectionReturn()); return ( {/* CARD 500 — THIS PROVIDER MUST WRAP BOTH THE GATE AND EVERYTHING BELOW IT, and that is the whole fix. The gate publishes whether a mandatory Akashic agreement is still outstanding; anything that would open a competing dialog reads it and stays silent until it is clear. What it prevents, measured on a real first sign-in: the Terms of Service gate and an ORGANIZATION-authorization dialog open at once, with the dismissible one on top — `elementFromPoint()` at the centre of **Accept** returned the other dialog's overlay, and 34% of the legal text was covered while it was displayed for acceptance. NOT a z-index fix. Both dialogs carried `z-index: 1100`, so the winner was decided by DOM order alone — an accident that could invert on any render change. Raising one keeps the accident and picks a side. The sequence is the defect: a patient was asked to let an organization into their health record before they had accepted, or could even READ, Akashic's own Terms. */} {/* CARD 638 — A PROVIDER-LEVEL WITHHOLDING WAS TRIED HERE AND REVERTED. DO NOT RE-ADD IT. `` used to wrap `` so that no patient-scoped request could be issued while an agreement was outstanding. It was reasoned about carefully, it had guards asserting it enclosed the providers and sat below the gate, and it BLANKED THE ENTIRE PATIENT PORTAL for every brand-new patient. THE MECHANISM, because the reasoning that missed it looked sound: `#akashic-overlay-root` — the container `ui/dialog` portals EVERY dialog into — is mounted inside the router, which is inside these providers. Withholding them removes the gate dialog's own portal target. The gate still ran and still opened, so Radix set `body { pointer-events: none }`, and the result was a blank page with no dialog, no error, and no way for the patient to accept anything. The person the gate exists for was the one person who could not answer it. THE LESSON: withholding a subtree is never local. It removes whatever else lives in it, and a portal target is invisible in the JSX you are reading. Verified in a browser 2026-08-23 on a fresh signup — a unit test cannot see it, because jsdom has no portal container either. The card's rule is enforced where it belongs and where it works: DashboardPage withholds the routed `` until the agreements check has ANSWERED, which is what actually stops the reads, because the fetching lives in the routed page rather than in these providers. */} {/* An invited patient's signup phone, filled into their own record once the agreements above are answered. */} {/* Card 128 sub-task 1 — the encounter note draft + the ONE active encounter must outlive navigation, so they mount ABOVE (the same reason AppVideoServiceProvider does). Do not move this inside the router: the draft is in-memory only and unmounting destroys it. */} ); } // Named for the mounted test of its hook order; the app itself only mounts it as a route element. export { RequireAuth }; export default App;