InfrastructureSPA Architecture

Developer Handbook

Practical guide for developers working with the SPA — adding modules, routes, sidebar items, debugging, common pitfalls, and testing.

👤 Aakash Pawar📅 Updated: Apr 30, 2026📁 SPA Architecture

Developer Handbook

A practical, step-by-step guide for developers working with the CrelioDashboard SPA. Use this as a quick reference when adding features, debugging issues, or onboarding.

Adding a New Module to the SPA

Step 1: Create the Routes and Menu File

Create a new file in RoutesAndMenus/:

src/modules/CrelioDashboard/RoutesAndMenus/myModuleRoutesAndMenu.tsx

Export two hooks — one for routes and one for sidebar:

import { CrelioRouteType, CrelioSidebarItem } from "../routeTypes";
import { useTranslation } from "react-i18next";
import React from "react";

// Lazy-load your components
const MyDashboard = React.lazy(() => import("src/components/MyModule/Dashboard"));
const MySettings = React.lazy(() => import("src/components/MyModule/Settings"));

export const useMyModuleRoutes = (): CrelioRouteType[] => {
  return [
    { path: "/my-module", Component: MyDashboard },
    { path: "/my-module/settings", Component: MySettings },
  ];
};

export const useMyModuleSidebarMenu = (): CrelioSidebarItem[] => {
  const { t } = useTranslation();
  return [
    { name: t("Dashboard"), icon: "fa-chart-line", route: "/my-module" },
    { name: t("Settings"), icon: "fa-cog", route: "/my-module/settings" },
  ];
};

Step 2: Register in routes.tsx

Open RoutesAndMenus/routes.tsx and:

  1. Import your hooks:
import { useMyModuleRoutes, useMyModuleSidebarMenu } from "./myModuleRoutesAndMenu";
  1. Call the hooks inside the Routes component:
const myModuleRoutes = useMyModuleRoutes();
const myModuleSidebarMenu = useMyModuleSidebarMenu();
  1. Add to the modules array:
const modules: Module[] = useMemo(() => [
  // ... existing modules
  { menu: myModuleSidebarMenu, routes: myModuleRoutes, selectedModule: t("MyModule") },
], [/* dependencies */]);

Step 3: Register in navigate.ts

Add your module's URL prefix to the moduleNames array:

export const moduleNames: string[] = [
  "registration", "accession", "operation",
  "finance", "crm", "admin", "inventory",
  "my-module",  // ← add here
];

This ensures the navigate() utility correctly prefixes intra-module URLs.

Step 4: Add Backend URL Mapping

In livehealth_4/decorators/utils.py, add the legacy-to-SPA URL mapping:

SPA_URL_MAPPER = {
    # ... existing mappings
    "/my-module-legacy/": "/crelio-dashboard/#/my-module",
}

Step 5: Update Session Helpers

In logins/utils/session_helpers.py, add the SPA login URL for your module's user role:

# For my-module staff
login_url = "/crelio-dashboard/#/my-module" if is_spa_enabled(user.labId) else "/my-module-legacy/"

Checklist

  • Created RoutesAndMenus/myModuleRoutesAndMenu.tsx with routes + sidebar hooks
  • Registered hooks in routes.tsx modules array
  • Added module name to moduleNames in navigate.ts
  • Added URL mapping in SPA_URL_MAPPER (backend)
  • Updated session helpers for login redirect (backend)
  • All route paths start with /my-module/ prefix
  • Components are lazy-loaded with React.lazy()

Adding Routes to an Existing Module

Adding a Simple Route

Add a new entry to your module's useXxxRoutes() hook:

export const useOperationsRoutes = (): CrelioRouteType[] => {
  return [
    // ... existing routes
    { path: "/operation/my-new-page", Component: MyNewPage },
  ];
};

Adding a Route with Permission Check

Use routeProps to restrict access:

{
  path: "/operation/admin-only-page",
  Component: AdminOnlyPage,
  routeProps: {
    requiredPermissions: ["canAccessAdminPage"],
  },
}

The AuthRoute component checks routeProps.requiredPermissions against the user's session permissions before rendering.

Adding a Redirect

Use the redirect route type:

{
  from: "/operation/old-path",
  to: "/operation/new-path",
  isRedirect: true,
}

Adding Sidebar Items

Simple Menu Item

{ name: t("My Page"), icon: "fa-file", route: "/operation/my-page" }
{
  name: t("Reports"),
  icon: "fa-chart-bar",
  submenuArr: [
    { name: t("Daily Report"), route: "/operation/reports/daily" },
    { name: t("Monthly Report"), route: "/operation/reports/monthly" },
  ],
}

Conditional Menu Item (Permission-Based)

{
  name: t("Admin Settings"),
  icon: "fa-cog",
  route: "/operation/admin-settings",
  hasPermission: () => sessionData?.isAdmin === true,
}

Hidden Menu Item

Use hidden: true for items that need a route but shouldn't appear in the sidebar:

{
  name: t("Hidden Page"),
  route: "/operation/hidden-page",
  hidden: true,
}

Within the Same Module

Use navigate() with relative-style paths — the utility auto-prefixes:

import { navigate } from "src/utils/navigate";

// From within Operations module
navigate("/testwise-waiting-list/all-tests");
// → resolves to /operation/testwise-waiting-list/all-tests

Cross-Module Navigation

Explicitly include the target module name:

navigate("/finance/dashboard");
// → no prefix added, navigates directly to Finance

With Route State

navigate({
  pathname: "/patient-details",
  state: { patientId: 123, fromModule: "registration" },
});

Replace History (No Back Button)

navigate("/settings", { replace: true });

Local Development

Starting the SPA Dev Server

cd apps/livehealth-frontend
yarn start local.crelioDashboard

This starts the SPA on port 3000 with hot-reload.

Starting a Single Module (Non-SPA)

yarn start local.Operations    # or Registration, Finance, etc.

Full Stack Setup

# Terminal 1: SPA frontend (port 3000)
cd apps/livehealth-frontend
yarn start local.crelioDashboard

# Terminal 2: Py2 backend (port 8000)
cd livehealthapp
python manage.py runserver 0.0.0.0:8000

# Terminal 3: Py3 backend (port 9000)
cd crelioapp
python manage.py runserver 0.0.0.0:9000

Enabling SPA Locally

Set the Redis flag for your local lab:

redis-cli SET is_spa_enabled_YOUR_LAB_ID True

Or via Django shell:

from django.core.cache import cache
cache.set("is_spa_enabled_YOUR_LAB_ID", True)

Debugging

Module Not Resolving

Symptom: Navigating to your module shows Registration (the fallback) instead of your module.

Checklist:

  1. Is the module name in the modules array in routes.tsx?
  2. Does the sidebar route's first path segment match your URL? (e.g., /my-module/dashboard should match a sidebar item starting with /my-module/)
  3. Is the selectedModule value in the modules array matching the expected name?

Route Not Rendering

Symptom: URL changes but the page shows PageNotFound.

Checklist:

  1. Does the route path exactly match the URL? Routes use exact={true}.
  2. Is the route in the correct module's useXxxRoutes() hook?
  3. Is the component exported correctly and lazy-loaded properly?

Symptom: navigate("/dashboard") goes to /dashboard instead of /operation/dashboard.

Checklist:

  1. Is the current URL within a recognized module? Check moduleNames in navigate.ts.
  2. Is isLabLogin() returning true?
  3. Is the target path accidentally matching a module name? (e.g., /admin won't be prefixed because it IS a module name)

SPA Not Loading

Symptom: Hitting /crelio-dashboard/ redirects to / (login page).

Checklist:

  1. Is is_spa_enabled_{lab_id} set to True in Redis?
  2. Is the user authenticated (valid labId in session)?
  3. Is crelio-dashboard.html deployed to build_assets/CrelioDashboard/build/?
  4. Try logging in as support user — they bypass the flag check.

Common Pitfalls

❌ Using window.location.href for Navigation

// DON'T — this triggers a full page reload and destroys SPA state
window.location.href = "/operation/dashboard";

// DO — use the navigate utility for in-app navigation
navigate("/operation/dashboard");

❌ Forgetting to Prefix Routes with Module Name

// DON'T — this will conflict with other modules using "/dashboard"
{ path: "/dashboard", Component: MyDashboard }

// DO — prefix all routes with your module name
{ path: "/my-module/dashboard", Component: MyDashboard }

❌ Module-Specific State in Global Redux

// DON'T — all modules share the same Redux store in SPA mode
// Avoid generic keys that other modules might also use
dispatch({ type: "SET_DASHBOARD_DATA", payload: data });

// DO — namespace your actions with the module name
dispatch({ type: "OPERATIONS/SET_DASHBOARD_DATA", payload: data });

❌ Initializing Settings in Component useEffect

// DON'T — this re-fetches on every module switch
useEffect(() => {
  fetchSettings();
}, []);

// DO — initialize in the SPA bootstrap (routes.tsx) so it runs once
// Or check if data already exists before fetching
useEffect(() => {
  if (!settings) fetchSettings();
}, []);

❌ Using history.push Directly

// DON'T — bypasses module-aware URL prefixing
import history from "src/utils/history";
history.push("/dashboard");

// DO — use navigate which handles module prefixing
import { navigate } from "src/utils/navigate";
navigate("/dashboard");

Testing

Testing SPA Mode vs v5 Mode

To compare behavior between SPA and v5 single-build:

TestSPA Modev5 Single-Build
Start serveryarn start local.crelioDashboardyarn start local.Operations
URL pattern/crelio-dashboard/#/operation/.../waitingList/... or /#/...
Redis flagis_spa_enabled_{lab_id} = TrueFlag absent or False
Module switchInstant (no reload)Full page reload

Quick Smoke Test

After making changes:

  1. Start the SPA dev server
  2. Log in and verify you land on /crelio-dashboard/#/{module}
  3. Switch between at least 2 modules via the sidebar — verify no page reload
  4. Use browser back/forward buttons — verify navigation works
  5. Use Cmd+K spotlight search — verify cross-module results
  6. Hard-refresh the page — verify the SPA reloads correctly at the same route

Testing a New Route

  1. Navigate to the new route directly via URL
  2. Navigate to it via sidebar click
  3. Navigate to it programmatically via navigate()
  4. Refresh the page on the new route — verify it loads correctly
  5. Use browser back button from the new route

Quick Reference

File Locations

WhatWhere
Module routes + sidebarsrc/modules/CrelioDashboard/RoutesAndMenus/xxxRoutesAndMenu.tsx
Module assemblysrc/modules/CrelioDashboard/RoutesAndMenus/routes.tsx
Route typessrc/modules/CrelioDashboard/routeTypes.ts
Navigate utilitysrc/utils/navigate.ts
History instancesrc/utils/history.ts
Sidebar componentsrc/modules/CrelioDashboard/Sidebar/sidebar.tsx
Module switchersrc/modules/CrelioDashboard/SwitchModuleDropdown/
SPA entry pointsrc/modules/CrelioDashboard/index.tsx
Build configapp-config.js, config-overrides.js

Key Imports

// Navigation
import { navigate } from "src/utils/navigate";

// Route types (for module hooks)
import { CrelioRouteType, CrelioSidebarItem, Module } from "../routeTypes";

// Redux state access
import { getReduxState } from "src/utils/helpers";

// History (only if you need location info, NOT for navigation)
import history from "src/utils/history";

On this page