import { designToken } from "@ekklesia/design-tokens";
import type { ModuleUiImporter } from "@ekklesia/module-ui-host";
import { Button } from "@ekklesia/ui";
import {
type ReactElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react";
import {
HostAuthError,
type HostAuthLanding,
type HostSignInCredentials,
requestPasswordReset,
resendVerificationEmail,
resetPassword,
signInWithEmail,
signOutHost,
} from "../auth-protocol.js";
import {
type DashboardRoute,
dashboardPath,
parseDashboardPath,
returnTargetFromSearch,
} from "../dashboard-routes.js";
import { resolveModuleRoute } from "../module-navigation.js";
import { parseReturnTarget } from "../return-target.js";
import {
bootstrapHost,
type DashboardNavigationEntry,
type EkklesiaHostConfig,
type HostAccountPreferences,
type HostBootstrap,
HostProtocolError,
loadHostPrincipal,
} from "../host-protocol.js";
import { AccountHomePage, AccountLoadFailedPage, AppearancePage } from "./AccountPages.js";
import { createMemoryAppearanceCache } from "./appearance-cache.js";
import {
type AppearancePreference,
createDetachedAppearancePreference,
} from "./appearance-preference.js";
import { ForgotPasswordForm } from "./ForgotPasswordForm.js";
import type { HostHistory } from "./host-history.js";
import { DisplaysSettingsPage } from "./DisplaysSettingsPage.js";
import { createBrowserHostViewport, type HostViewport } from "./host-viewport.js";
import { ModuleUnavailablePage, NotFoundPage, OverviewPage } from "./HostPages.js";
import { LoginForm } from "./LoginForm.js";
import { ModuleUiFrame } from "./ModuleUiFrame.js";
import { createModuleUiNavigation } from "./module-ui-navigation.js";
import { ResetPasswordForm } from "./ResetPasswordForm.js";
import { ShellLayout } from "./ShellLayout.js";
import { resolveSettingsPage, type SettingsPage, settingsPageTitle } from "./settings-catalog.js";
import {
DiscoverPage,
ModuleSettingsPage,
SettingsHomePage,
SettingsLoadFailedPage,
SettingsSectionPage,
settingsBack,
type SettingsReturn,
SettingsUnavailablePage,
} from "./SettingsPages.js";
import { createMemorySidebarPreference, type SidebarPreference } from "./sidebar-preference.js";
import { ResetLinkInvalid, VerificationNotice, VerificationResult } from "./VerificationScreens.js";
type AuthenticatedPrincipal = Extract<
HostBootstrap["principal"],
{ readonly kind: "authenticated" }
>;
type BootstrapState =
| { readonly status: "loading" }
| { readonly status: "ready"; readonly bootstrap: HostBootstrap }
| { readonly status: "anonymous" }
| { readonly status: "access-denied"; readonly principal: AuthenticatedPrincipal }
| { readonly status: "failed" }
| { readonly status: "signing-out" }
| { readonly status: "sign-out-failed" };
type ResolvedBootstrapState = Extract<
BootstrapState,
{ readonly status: "ready" | "anonymous" | "access-denied" | "failed" }
>;
/**
* Which host-owned screen an unauthenticated visitor sees.
*
* A small explicit union rather than a client-side router: these four screens are the entirety of
* the platform's anonymous surface, and a routing library would be a dependency and an extra URL
* authority for a state machine that fits on a page. Only `reset-password` carries anything, and
* what it carries came from an emailed link the shell has already stripped from the address bar.
*
* None of these states is authenticated, and none can become authenticated: the shell renders the
* Dashboard only after `/api/v1/principal` says so, exactly as before.
*/
type AuthView =
| { readonly kind: "sign-in" }
| { readonly kind: "forgot-password" }
| { readonly kind: "verification-required"; readonly email: string }
| { readonly kind: "verification-result"; readonly outcome: VerificationOutcome }
| { readonly kind: "reset-password"; readonly token: string }
| { readonly kind: "reset-link-invalid" };
type VerificationOutcome =
| { readonly kind: "verified" }
| { readonly kind: "failed"; readonly expired: boolean };
function authViewFromLanding(landing: HostAuthLanding | undefined): AuthView {
if (!landing) {
return { kind: "sign-in" };
}
if (landing.kind === "verified") {
return { kind: "verification-result", outcome: { kind: "verified" } };
}
if (landing.kind === "verification-failed") {
return {
kind: "verification-result",
outcome: { expired: landing.expired, kind: "failed" },
};
}
if (landing.kind === "reset-password") {
return { kind: "reset-password", token: landing.token };
}
return { kind: "reset-link-invalid" };
}
export interface HostAppProps {
/** Must be a stable reference: bootstrap re-runs whenever it changes, so a fresh object literal
* built during render would re-fetch forever. The browser entry point builds it once at module
* scope. */
readonly config: EkklesiaHostConfig;
/**
* What an emailed verification or reset link asked this browser to show, already parsed.
*
* Supplied by the entry point rather than read here, so this component still touches no browser
* global. It only ever selects among the host's own anonymous screens: it can name no destination,
* so there is nothing an open redirect could use.
*/
readonly authLanding?: HostAuthLanding;
/**
* The address bar (ADR 0057). The current path decides which page is open, so a refresh, a
* bookmark, and Back and Forward all land where the person was. The entry point passes the
* browser's; tests pass an in-memory one. Must be a stable reference.
*/
readonly history: HostHistory;
/** Names the browser tab after the open page. Supplied by the entry point, which owns `document`;
* left unset, the shell names nothing. */
readonly setDocumentTitle?: (title: string) => void;
/**
* Whether the page is laid out for a phone (ADR 0058). The entry point passes the browser's media
* query; left unset, the wide layout. Must be a stable reference.
*/
readonly viewport?: HostViewport;
/**
* Where the desktop sidebar's expanded or collapsed mode is remembered (ADR 0058). The entry point
* passes this browser's storage; left unset, the mode lasts only as long as this component.
*/
readonly sidebarPreference?: SidebarPreference;
/**
* The signed-in person's appearance and motion as this page applies them (ADR 0062). The entry
* point passes one over the document's theme controller and this browser's pre-paint cache; left
* unset, choices apply to nothing and last only as long as this component. Must be a stable
* reference.
*/
readonly appearance?: AppearancePreference;
/** Test-only seam, forwarded to {@link ModuleUiFrame}. Production composition leaves it unset. */
readonly importModule?: ModuleUiImporter;
}
/**
* The host's full-page messages (loading, failure, sign-out), in the platform's semantic tokens
* (ADR 0039). The signed-in Dashboard's own frame is `ShellLayout` (ADR 0058).
*/
const shellStyles = {
actions: { display: "flex", gap: designToken("spacing.sm"), justifyContent: "center" },
message: {
color: designToken("text.secondary"),
display: "grid",
fontFamily: designToken("typography.family-sans"),
gap: designToken("spacing.md"),
justifyItems: "center",
margin: 0,
padding: designToken("spacing.lg"),
textAlign: "center",
},
} as const;
/** Identity, not placement: moving a module between groups does not reset what is open. */
function moduleKey(entry: DashboardNavigationEntry): string {
return `${entry.descriptor.id}@${entry.descriptor.version}`;
}
function allEntries(bootstrap: HostBootstrap): readonly DashboardNavigationEntry[] {
return [
...bootstrap.navigation.groups.flatMap((group) => group.modules),
...bootstrap.navigation.ungrouped,
];
}
function SettingsPageView(props: {
readonly config: EkklesiaHostConfig;
readonly page: SettingsPage;
readonly history: HostHistory;
readonly onRetry: () => void;
readonly returnTo: SettingsReturn;
}): ReactElement {
const { config, page, history, returnTo } = props;
switch (page.kind) {
case "home":
return ;
case "section":
return page.section === "displays" ? (
) : (
);
case "discover":
return ;
case "module":
return ;
case "unavailable":
return ;
case "load-failed":
return ;
case "not-found":
return ;
}
}
/** A stable identity for "which page is showing", so a non-canonical spelling of the same page is
* not a page change. */
function routeKey(route: DashboardRoute): string {
return route.kind === "not-found" ? "not-found" : dashboardPath(route);
}
/**
* A `?return=` value the Settings pages may use (ADR 0063), or `undefined`. `parseReturnTarget`
* accepts only a canonical same-origin Dashboard address; a module's address is additionally checked
* against the modules actually running here and the routes each declares, so Back never points at a
* module that is not running or at a page it does not have.
*/
function usableReturnTarget(
raw: string | undefined,
entries: readonly DashboardNavigationEntry[],
): string | undefined {
const destination = parseReturnTarget(raw);
if (destination === undefined) {
return undefined;
}
if (destination.kind !== "module") {
return raw;
}
const entry = entries.find((candidate) => candidate.descriptor.id === destination.moduleId);
return entry !== undefined && resolveModuleRoute(entry.pages, destination.path) !== undefined
? raw
: undefined;
}
/**
* One mounted module UI and the navigation capability it is handed (ADR 0063). The capability is
* created once per mount and reads the address bar live, so it is the same object for the module's
* whole life however often it moves between its pages, and the frame never remounts because of it.
*/
function MountedModule(props: {
readonly bootstrap: HostBootstrap;
readonly config: EkklesiaHostConfig;
readonly entry: DashboardNavigationEntry;
readonly history: HostHistory;
readonly importModule?: ModuleUiImporter;
readonly onAuthorizationResponse: (status: 401 | 403) => void;
}): ReactElement {
const { bootstrap, entry, history } = props;
const settingsRef = useRef(bootstrap.settings);
settingsRef.current = bootstrap.settings;
const moduleId = entry.descriptor.id;
const navigation = useMemo(
() =>
createModuleUiNavigation({
hasSettings: () => {
const settings = settingsRef.current;
return (
settings !== "unavailable" &&
settings.modules.some((module) => module.id === moduleId && module.available)
);
},
history,
moduleId,
pages: entry.pages,
}),
[entry.pages, history, moduleId],
);
return (
);
}
function HostMessage(props: {
readonly children: string;
readonly testId: string;
readonly actions?: ReactElement;
}): ReactElement {
return (
{props.children}
{props.actions}
);
}
/**
* Principal first, navigation second. A 403 after an authenticated principal is authorization, not
* evidence that the session vanished. Re-read the principal once only to close the narrow race where
* a session expires between those two sequential requests.
*/
async function resolveBootstrapState(
config: EkklesiaHostConfig,
signal: AbortSignal,
): Promise {
try {
const bootstrap = await bootstrapHost(config, signal);
// Only a person signs into the Dashboard. The test is `=== "authenticated"` and never "not
// anonymous", so a principal class added later cannot quietly become a Dashboard login — which
// is precisely how a paired display device would otherwise have walked in (ADR 0036). In
// practice `/api/v1/principal` never reports a device at all; this is the second lock.
return bootstrap.principal.kind === "authenticated"
? { bootstrap: { ...bootstrap, principal: bootstrap.principal }, status: "ready" }
: { status: "anonymous" };
} catch (error) {
if (error instanceof HostProtocolError && error.code === "HOST_UNAUTHENTICATED") {
return { status: "anonymous" };
}
if (error instanceof HostProtocolError && error.code === "HOST_ACCESS_DENIED") {
try {
const principal = await loadHostPrincipal(config, signal);
return principal.kind === "authenticated"
? { principal, status: "access-denied" }
: { status: "anonymous" };
} catch {
return { status: "failed" };
}
}
return { status: "failed" };
}
}
/**
* The Ekklesia Dashboard shell and its host-owned authentication boundary.
*
* The login form replaces the protected application in place. There is no redirect parameter,
* return value, or location assignment in this shell, so the current internal Dashboard URL simply
* remains the current URL through sign-in — a deep link opens on its own page once the person has
* signed in — and no external/protocol-relative value can become a navigation sink. The shell only
* ever moves between its own paths (ADR 0057). Modules receive the same context after
* authentication; they neither render platform login nor receive sign-in/sign-out controls.
*/
export function HostApp(props: HostAppProps): ReactElement {
const [state, setState] = useState({ status: "loading" });
const [bootstrapAttempt, setBootstrapAttempt] = useState(0);
const activeTransitionRef = useRef(undefined);
const revalidationRef = useRef(undefined);
const generationRef = useRef(0);
const mountedRef = useRef(true);
const mainRef = useRef(null);
const shownRouteRef = useRef(undefined);
const { config, history, importModule, setDocumentTitle } = props;
// Fallbacks for a composition that supplies neither; created once, so they stay stable.
const [fallbackViewport] = useState(() => createBrowserHostViewport(undefined));
const [fallbackPreference] = useState(() => createMemorySidebarPreference());
const [fallbackAppearance] = useState(() =>
createDetachedAppearancePreference(createMemoryAppearanceCache()),
);
const viewport = props.viewport ?? fallbackViewport;
const sidebarPreference = props.sidebarPreference ?? fallbackPreference;
const appearance = props.appearance ?? fallbackAppearance;
const subscribe = useCallback((listener: () => void) => history.subscribe(listener), [history]);
// The whole address, so a change of query alone (a new place to return to) re-renders too.
const readAddress = useCallback(() => `${history.pathname()}${history.search()}`, [history]);
const address = useSyncExternalStore(subscribe, readAddress, readAddress);
const pathname = address.split("?")[0] ?? "/";
const search = address.slice(pathname.length);
const route = useMemo(() => parseDashboardPath(pathname), [pathname]);
// Initialized once from the landing the entry point parsed. A later sign-out or a "back to sign
// in" action moves it back to the form; nothing re-reads the URL, so a stale emailed parameter
// cannot resurrect a screen the person has left.
const [authView, setAuthView] = useState(() => authViewFromLanding(props.authLanding));
const beginTransition = useCallback(() => {
activeTransitionRef.current?.abort();
revalidationRef.current?.abort();
revalidationRef.current = undefined;
const controller = new AbortController();
activeTransitionRef.current = controller;
generationRef.current += 1;
return { controller, generation: generationRef.current };
}, []);
const isCurrent = useCallback(
(controller: AbortController, generation: number): boolean =>
mountedRef.current &&
!controller.signal.aborted &&
activeTransitionRef.current === controller &&
generationRef.current === generation,
[],
);
/**
* Every resolved bootstrap reconciles the page's appearance with the account (ADR 0062): the
* account's preferences replace whatever this browser cached, so a choice made on another device
* is in effect here from this load on. A signed-out page follows the system and keeps no cached
* choice, so the next person to use this browser does not inherit one. An account that could not
* be loaded leaves the cached choice alone rather than guessing.
*/
const commitResolvedState = useCallback(
(resolved: ResolvedBootstrapState): void => {
if (resolved.status === "ready" && resolved.bootstrap.account !== "unavailable") {
appearance.apply(resolved.bootstrap.account.preferences);
} else if (resolved.status === "anonymous") {
appearance.forget();
}
setState(resolved);
},
[appearance],
);
/** A saved preference, as the account answered, kept in the bootstrap's copy of the account. */
const recordSavedPreferences = useCallback((preferences: HostAccountPreferences): void => {
setState((current) =>
current.status === "ready" && current.bootstrap.account !== "unavailable"
? {
...current,
bootstrap: {
...current.bootstrap,
account: { ...current.bootstrap.account, preferences },
},
}
: current,
);
}, []);
// A non-canonical spelling of a real page (`/example/`) is corrected in place, adding no
// history entry, so what is bookmarked or shared next is the one canonical address.
useEffect(() => {
if (route.kind !== "not-found" && dashboardPath(route) !== pathname) {
// The query is kept: it is where to return to (ADR 0063), and respelling the path must not
// lose it.
history.replace(`${dashboardPath(route)}${search}`);
}
}, [history, pathname, route, search]);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
activeTransitionRef.current?.abort();
revalidationRef.current?.abort();
};
}, []);
useEffect(() => {
// The value is an explicit manual-retry generation; reading it is what makes Retry re-run this
// otherwise config-driven bootstrap effect.
void bootstrapAttempt;
const { controller, generation } = beginTransition();
setState({ status: "loading" });
void resolveBootstrapState(config, controller.signal).then((resolved) => {
if (!isCurrent(controller, generation)) {
return;
}
if (resolved.status === "failed") {
console.error("[ekklesia] host bootstrap failed");
}
commitResolvedState(resolved);
activeTransitionRef.current = undefined;
});
return () => {
if (activeTransitionRef.current === controller) {
controller.abort();
activeTransitionRef.current = undefined;
}
};
}, [beginTransition, bootstrapAttempt, commitResolvedState, config, isCurrent]);
const authenticate = useCallback(
async (credentials: HostSignInCredentials, formSignal: AbortSignal): Promise => {
const { controller, generation } = beginTransition();
const abortFromForm = (): void => controller.abort();
formSignal.addEventListener("abort", abortFromForm, { once: true });
try {
await signInWithEmail(config, credentials, controller.signal);
const resolved = await resolveBootstrapState(config, controller.signal);
if (!isCurrent(controller, generation)) {
throw new HostAuthError("AUTH_REQUEST_FAILED");
}
if (resolved.status === "failed") {
console.error("[ekklesia] post-sign-in principal resolution failed");
}
commitResolvedState(resolved);
activeTransitionRef.current = undefined;
if (resolved.status === "anonymous" || resolved.status === "failed") {
throw new HostAuthError("AUTH_REQUEST_FAILED");
}
} finally {
formSignal.removeEventListener("abort", abortFromForm);
if (activeTransitionRef.current === controller) {
activeTransitionRef.current = undefined;
}
}
},
[beginTransition, commitResolvedState, config, isCurrent],
);
const signOut = useCallback(async (): Promise => {
const { controller, generation } = beginTransition();
// The authenticated subtree disappears before the request starts. That synchronously schedules
// ModuleUiFrame cleanup, which invokes the module's unmount and closes its realtime subscriptions.
setState({ status: "signing-out" });
try {
await signOutHost(config, controller.signal);
const principal = await loadHostPrincipal(config, controller.signal);
if (!isCurrent(controller, generation)) {
return;
}
// Returning to the anonymous shell always returns to the sign-in form. Without this, signing
// out while a verification or reset screen was the last anonymous view would show that screen
// again, describing an emailed link the person has long since finished with.
setAuthView({ kind: "sign-in" });
if (principal.kind === "anonymous") {
appearance.forget();
setState({ status: "anonymous" });
} else {
setState({ status: "sign-out-failed" });
}
} catch {
if (isCurrent(controller, generation)) {
console.error("[ekklesia] sign out failed");
setState({ status: "sign-out-failed" });
}
} finally {
if (activeTransitionRef.current === controller) {
activeTransitionRef.current = undefined;
}
}
}, [appearance, beginTransition, config, isCurrent]);
const onAuthorizationResponse = useCallback(
(status: 401 | 403): void => {
// Module handlers are not the platform auth authority: one may emit an ordinary local 401,
// while Ekklesia authorization uses 403 for both anonymous and authenticated denials. Re-read
// principal once, de-duplicated across a burst of either status. Only an anonymous answer
// expires the shell; an authenticated answer leaves the module's original response alone. A
// principal failure is an unavailable platform, never a fabricated logout.
void status;
if (revalidationRef.current) {
return;
}
const controller = new AbortController();
const observedGeneration = generationRef.current;
revalidationRef.current = controller;
void loadHostPrincipal(config, controller.signal)
.then((principal) => {
if (
controller.signal.aborted ||
!mountedRef.current ||
generationRef.current !== observedGeneration
) {
return;
}
if (principal.kind === "anonymous") {
const transition = beginTransition();
transition.controller.abort();
activeTransitionRef.current = undefined;
setAuthView({ kind: "sign-in" });
appearance.forget();
setState({ status: "anonymous" });
}
})
.catch(() => {
if (
controller.signal.aborted ||
!mountedRef.current ||
generationRef.current !== observedGeneration
) {
return;
}
const transition = beginTransition();
transition.controller.abort();
activeTransitionRef.current = undefined;
console.error("[ekklesia] session revalidation failed");
setState({ status: "failed" });
})
.finally(() => {
if (revalidationRef.current === controller) {
revalidationRef.current = undefined;
}
});
},
[appearance, beginTransition, config],
);
const openEntry =
state.status === "ready" && route.kind === "module"
? allEntries(state.bootstrap).find((entry) => entry.descriptor.id === route.moduleId)
: undefined;
// Which of the open module's own routes this is (ADR 0063): its root, a declared page, or a route
// beneath one. Anything else is the host's not-found, and the module is not mounted for it.
const openModuleRoute =
openEntry !== undefined && route.kind === "module"
? resolveModuleRoute(openEntry.pages, route.path)
: undefined;
const settingsPage =
state.status === "ready" &&
(route.kind === "settings" ||
route.kind === "settings-section" ||
route.kind === "discover" ||
route.kind === "module-settings")
? resolveSettingsPage(
route,
state.bootstrap.settings,
new Set(allEntries(state.bootstrap).map((entry) => entry.descriptor.id)),
)
: undefined;
const pageTitle =
state.status !== "ready"
? undefined
: settingsPage !== undefined
? settingsPageTitle(settingsPage)
: route.kind === "overview"
? "Overview"
: route.kind === "module"
? openEntry === undefined
? "This page isn’t available"
: openModuleRoute === undefined
? "Page not found"
: openModuleRoute.page === undefined
? openEntry.name
: `${openModuleRoute.page.label} · ${openEntry.name}`
: route.kind === "account"
? "Account"
: route.kind === "account-section"
? "Appearance"
: "Page not found";
const documentTitle = state.status === "anonymous" ? "Sign in" : pageTitle;
useEffect(() => {
if (documentTitle !== undefined) {
setDocumentTitle?.(documentTitle);
}
}, [documentTitle, setDocumentTitle]);
// Moving to another page moves keyboard and screen-reader focus to the new page's content, as a
// full page load would have. The first page shown after sign-in or a refresh is left alone, and a
// canonical respelling of the same page is not a move.
const currentRouteKey = routeKey(route);
useEffect(() => {
if (state.status !== "ready") {
shownRouteRef.current = undefined;
return;
}
if (shownRouteRef.current !== undefined && shownRouteRef.current !== currentRouteKey) {
// A new page starts at its top, as a full page load would, and focusing it must not scroll
// it: focus() alone would scroll the page's top to the very top of the viewport, beneath
// the phone's sticky header, so every page opened from the drawer started under it.
const main = mainRef.current;
main?.focus({ preventScroll: true });
main?.ownerDocument.scrollingElement?.scrollTo?.({ left: 0, top: 0 });
}
shownRouteRef.current = currentRouteKey;
}, [currentRouteKey, state.status]);
const retryBootstrap = (): void => setBootstrapAttempt((current) => current + 1);
if (state.status === "loading") {
return Loading Ekklesia…;
}
if (state.status === "anonymous") {
const returnToSignIn = (): void => setAuthView({ kind: "sign-in" });
if (authView.kind === "forgot-password") {
return (
requestPasswordReset(config, email, signal)}
onReturnToSignIn={returnToSignIn}
/>
);
}
if (authView.kind === "verification-required") {
return (
resendVerificationEmail(config, email, signal)}
onReturnToSignIn={returnToSignIn}
/>
);
}
if (authView.kind === "verification-result") {
return ;
}
if (authView.kind === "reset-password") {
return (
resetPassword(config, input, signal)}
onReturnToSignIn={returnToSignIn}
token={authView.token}
/>
);
}
if (authView.kind === "reset-link-invalid") {
return ;
}
return (
setAuthView({ email, kind: "verification-required" })}
onForgotPassword={() => setAuthView({ kind: "forgot-password" })}
/>
);
}
if (state.status === "failed") {
return (
Retry} testId="host-error">
Ekklesia could not be reached.
);
}
if (state.status === "signing-out") {
return Signing out…;
}
if (state.status === "sign-out-failed") {
return (
}
testId="host-sign-out-error"
>
Ekklesia could not sign you out. Try again.
);
}
if (state.status === "access-denied") {
return (
void signOut()}>
Sign out
}
testId="host-access-denied"
>
You are signed in, but do not have access to this Dashboard.
);
}
const { bootstrap } = state;
return (
void signOut()}
pageTitle={pageTitle}
route={route}
settings={bootstrap.settings}
sidebarPreference={sidebarPreference}
viewport={viewport}
>
{settingsPage !== undefined ? (
allEntries(bootstrap).find((entry) => entry.descriptor.id === moduleId)?.name,
target: usableReturnTarget(returnTargetFromSearch(search), allEntries(bootstrap)),
}}
/>
) : route.kind === "account" || route.kind === "account-section" ? (
bootstrap.account === "unavailable" ? (
) : route.kind === "account" ? (
void signOut()}
/>
) : (
onAuthorizationResponse(403)}
/>
)
) : route.kind === "overview" ? (
) : route.kind === "module" ? (
openEntry === undefined ? (
// Installed but not running, or not known at all: never mounted, whatever the path.
) : openModuleRoute === undefined ? (
) : (
)
) : (
)}
);
}