Skip to main content

Lake Pulse (ILM)

Lake Pulse is the Integrated Lake Monitoring (ILM) module. It gives a portfolio view of monitored lakes, then drills into a single lake for water availability (bathymetry / volume), water quality (in-situ ASV surveys and satellite-derived indices), and time-series point trends.

The feature ships in two places from the same library (libs/ilm/src/):

  • Inside the main production app at /lakepulse.
  • As the dedicated lakepulse standalone app — a single-product build whose route table, navigation helper, and store context are scoped to ILM.

Overview

The ILM library renders a portfolio of lakes and a per-lake detail experience. All state lives in a single React context provider (IlmStoreProvider) that orchestrates lake list loading, lake selection, and every downstream data fetch (bathymetry, satellite, ASV quality, point trends, water-availability trend).

Location: libs/ilm/src/

Route(s): /lakepulse (portfolio index) and /lakepulse/:lakeId (lake detail)

Registered by: apps/production/src/routes/routes.js and apps/lakepulse/src/routes/routes.js

Standalone app: lakepulse (standaloneAppName="lakepulse")

Sidebar entry: YOUR_PORTFOLIO/lakepulse (libs/shared/src/helper/lakePulseNavigationHelperInstance.js), access SidebarAccess.OPEN, permissionId: 'ILM'.

Tab-level permissions: ILM_WATER_QUALITY, ILM_SATELLITE_WATER_QUALITY, ILM_HIDE_HYDROGRAPHY, GWI (see Visibility & Permissions).


Routing

Route table

Both the main app and the standalone app register an identical nested route. The index child renders the portfolio; the :lakeId child renders the lake detail — the same <IlmPage /> element handles both, branching on the URL param.

// apps/lakepulse/src/routes/routes.js
{
path: 'lakepulse',
children: [
{ index: true, element: <IlmPage /> }, // /lakepulse → PortfolioPage
{ path: ':lakeId', element: <IlmPage /> }, // /lakepulse/:lakeId → LakeDetailPage
],
}

IlmPage selects the sub-page from the route param — there is no separate route component:

// libs/ilm/src/IlmPage.jsx
function IlmContent() {
const { lakeId } = useParams();
return lakeId ? <LakeDetailPage /> : <PortfolioPage />;
}

export default function IlmPage() {
return (
<SubPageWrapper>
<Box sx={{ width: '100%' }}>
<IlmStoreProvider>
<FixedBar bar={<LakeHeader />}>
<IlmContent />
</FixedBar>
</IlmStoreProvider>
</Box>
</SubPageWrapper>
);
}
Navigation to a lake

PortfolioPage navigates with a relative path — navigate(lake.unitId) (libs/ilm/src/components/PortfolioPage.jsx) — so from /lakepulse it lands on /lakepulse/<unitId>. The detail page's "Back to Portfolio" button uses navigate(-1) (LakeDetailPage.jsx).


Standalone app specifics

Lake Pulse follows the standalone-app pattern documented in Standalone Apps. Its three ingredients:

1. Per-app login check

// apps/lakepulse/src/bootstrap.jsx
import { createValidateLogin } from '@aquagen-mf-webapp/components/pages/splash/splashController';
import { LakePulseNavigationHelper } from '@aquagen-mf-webapp/shared/helper/lakePulseNavigationHelperInstance';
const validateLogin = createValidateLogin(LakePulseNavigationHelper);

2. Per-app navigation helper (navInstance)

// apps/lakepulse/src/routes/routes.js
<MainLayout navInstance={LakePulseNavigationHelper} />

LakePulseNavigationHelper (libs/shared/src/helper/lakePulseNavigationHelperInstance.js) is a singleton (.i getter) that defines the app's brand (Lake / Pulse, brandColor: '#2B6275'), nav colors, and route map:

// lakePulseNavigationHelperInstance.js
routes = {
YOUR_PORTFOLIO: '/lakepulse',
AQUAGPT: '/aquagpt',
ALERTS: '/alerts/WATER_ALERTS',
REPORTS: '/reports',
};

3. Store name — standaloneAppName="lakepulse"

// apps/lakepulse/src/bootstrap.jsx
<AppStoreContextProvider validateLogin={validateLogin} standaloneAppName="lakepulse">

Inside AppStore (libs/shared/src/store/AppStore.js) this sets activeWaterContext = standaloneAppName ?? waterContext. Lake Pulse has no dedicated derived flag (unlike isRwiNativeApp / isFromUwms); it simply becomes the active water context and selects the nav helper.

bootstrap vs app split

lakepulse has the cleanest current-pattern split. bootstrap.jsx owns all startup wiring (MSAL, Google OAuth, theme, store provider, router); app.jsx is routes-only:

// apps/lakepulse/src/app/app.jsx
export function App() {
const element = useRoutes(appRouter);
return element;
}

Branding is title-only

Per the standalone pattern, the only static per-app branding is the <title> tag — <title>Lakepulse</title> (apps/lakepulse/src/index.html). Theme, favicon, and stylesheet are shared; colour theming is applied at runtime via the nav helper's brandColor / navColors.


Key Features

1. Multi-lake portfolio & selection

PortfolioPage shows a summary strip (Total Assets / Assets in Good Condition / Assets at Risk), a search box (by lake name or location), and a responsive grid of LakeGridCards. Health buckets are derived from overallHealth, falling back to wqiClass (PortfolioPage.jsx).

Lake switching is centralised in selectLake(newUnitId) (IlmStore.js), which:

  • Guards no-op selects (newUnitId === selectedLakeUnitId returns early).
  • Clears all stale data from the previous lake (satellite, ASV, boundary, bathymetry, point trends, water-availability trend).
  • Sets selectedLakeUnitId, bathymetryUnitId, and asvUnitId.
  • Matches a satellite unit by index against satelliteUnits (loaded from the login response's ID_SATELLITE_LAKE category), falling back to satelliteUnits[0].
  • Fetches ASV dates, then bathymetry; only fetches point trends if bathymetry metadata contains track points; always fetches the water-availability trend.

2. Bathymetry, volume & hydrography (Water Availability)

The Water Availability tab (HydrographyPanel) shows metric cards — Lake Volume (kL), Surface Area (m²), Water Depth (m), each with a "Theoretical Maximum" — beside a bathymetry map that toggles between 3D (Hydrography3D, Plotly surface) and 2D (Hydrography2D, Leaflet depth points). A Baseline / Periodic Trends toggle drives the date picker and, in Periodic mode, renders WaterAvailabilityTrendChart.

Metrics come straight from the bathymetry result: volumeM3, areaM2, hAbs, maxVolumeM3, maxAreaM2, maxDepthM (HydrographyPanel.jsx).

3. Water Quality Index — in-situ ASV surveys

The Water Quality Index tab (WaterQualityPanel) renders parameter cards from volumeAndBathymetryData.meta.qualityParams and an ASV survey map (AsvWaterQualityMap). CPCB safe ranges backfill missing thresholds via getCpcbDefaults(key) (WaterQualityPanel.jsx) — covering turbidity (NTU, safeMax 10), pH (6.5–8.5), TDS (mg/L, safeMax 500), DO, conductivity, BOD, COD, ORP, ammonia, nitrate, phosphate, coliforms, temperature. In Periodic Trends mode, when track points exist, PointTrendsPanel is shown below the map.

4. Satellite water quality

The Satellite Water Quality tab (SatelliteWaterQualityPanel) renders parameter cards from meta.satelliteParams and a WaterRiskHeatMap (Leaflet). Per-parameter averages are aggregated from the satellite data points (SatelliteWaterQualityPanel.jsx).

PointTrendsPanel (Recharts) plots per-parameter survey trends and a WQI score trend, toggled by a params / score view. Parameter labels are mapped in PARAM_LABELS — including turbidity, tss (TSS), bod, cod, do_saturation, orp_value, p_h (pH), etc. (PointTrendsPanel.jsx). It reads pointTrendsData (data.points[]) and surveyWqiTrend (data.surveyWqiTrend[]).


Architecture

Data Flow

IlmPage

IlmStoreProvider (Context)

IlmController.<method>()

IlmDataSource.<method>()

apiClient.get(Urls.<endpoint>) → Backend

Update IlmStore state

Re-render Portfolio / LakeDetail components

Component composition

IlmPage
├── LakeHeader (FixedBar title)
├── PortfolioPage (index route)
│ ├── SummaryStrip
│ └── LakeGridCard[]
└── LakeDetailPage (:lakeId route)
├── LakeCard
│ ├── LakeImageCarousel
│ └── LakeMapPreview (Leaflet)
├── InfoRow (alert / insight)
└── DataTabs
├── HydrographyPanel (Water Availability)
│ ├── Hydrography3D (Plotly)
│ ├── Hydrography2D (Leaflet)
│ └── WaterAvailabilityTrendChart (Recharts)
├── WaterQualityPanel (Water Quality Index)
│ ├── AsvWaterQualityMap (Leaflet)
│ └── PointTrendsPanel (Recharts)
├── SatelliteWaterQualityPanel (Satellite Water Quality)
│ └── WaterRiskHeatMap (Leaflet)
└── GwiPage (GWI — from libs/gwi)

Key components

1. IlmStoreProvider

  • Purpose: All ILM state and data orchestration.
  • Location: libs/ilm/src/store/IlmStore.js
  • State groups: lake selection, satellite, ASV water quality, point trends, water-availability trend, hydrography/bathymetry.
  • Derives lakeBoundary from the bathymetry meshGrid (IlmStore.js, computeLakeBoundary) for use across the map tabs.

2. Datatabs

  • Purpose: Tab bar + panels; filters/locks tabs by permission.
  • Location: libs/ilm/src/components/Datatabs.jsx
  • On first resolve, jumps to the first unlocked tab (Datatabs.jsx).

3. HydrographyPanel / Hydrography3D / Hydrography2D

  • 3D surface (react-plotly.js) and 2D depth-point map (react-leaflet), toggled at bottom-right.

4. WaterQualityPanel / AsvWaterQualityMap / PointTrendsPanel

  • In-situ ASV parameter cards, survey map, and periodic point trends.

5. SatelliteWaterQualityPanel / WaterRiskHeatMap

  • Satellite parameter cards and a Leaflet risk heat map.

API Integration

All endpoints route through IlmControllerIlmDataSourceapiClient.get(Urls.<name>). The URL constants are defined in libs/shared/src/services/api/urls.js.

Store actionControllerUrls keyEndpointParams
getLakeList()getLakeListlakeListwri/lakeList
calculateVolumeAndBathymetry()calculateVolumeAndBathymetrycalculateVolumeAndBathymetrywri/bathymetryResult{ unitId, date? }
getHydrographyData()getHydrographyDatacategoryDataV2deviceDataV2params
fetchAvailableDates()getDatesListwriDatesListwri/datesList{ unitId }
fetchSatelliteData()getSatelliteDatawriSatelliteDatawri/satelliteData{ date, unitId }
fetchAsvDates()getAsvDatesasvDateswri/asv/dates{ unitId }
fetchAsvQuality()getAsvQualityasvQualitywri/asv/quality{ unitId, date }
fetchPointTrends()getPointTrendsilmPointTrendswri/ilm/pointTrends{ unitId }
fetchWaTrend()getTrendWaterAvailabilityilmTrendWaterAvailabilitywri/ilm/trendWaterAvailability{ unitId }

Source references: libs/ilm/src/dataSource/ilmDataSource.js, libs/ilm/src/controller/ilmController.js, libs/shared/src/services/api/urls.js.

Example: bathymetry response shape (as consumed)

// Consumed by Hydrography3D/2D and HydrographyPanel
volumeAndBathymetryData = {
volumeM3: 12500,
areaM2: 8400,
hAbs: 3.2,
maxVolumeM3: 15000,
maxAreaM2: 9000,
maxDepthM: 4.1,
meshGrid: { x: [...lngs], y: [...lats], z: [[...depths]] },
meta: {
date: '25/02/2026',
trackPoints: [{ latitude, longitude, /* ... */ }],
qualityParams: [{ key: 'turbidity', name: 'Turbidity', unit: 'NTU', safeMax: 10 }],
satelliteParams: [/* ... */],
}
}

Edge Cases

Lake Pulse renders many independent async slices, each with its own loading / empty / error handling. The following are enforced in code.

Loading states

SliceLoading UIReference
Lake list (portfolio)6 Skeleton cardsPortfolioPage.jsx
Bathymetry 3DCustomLoader (centered)Hydrography3D.jsx
Bathymetry 2DCircularProgressHydrography2D.jsx
ASV mapCircularProgressAsvWaterQualityMap.jsx
Point trends"Loading trend data…"PointTrendsPanel.jsx
Water-availability trendspinnerWaterAvailabilityTrendChart.jsx
Date pickers"Loading..." labelHydrographyPanel.jsx, WaterQualityPanel.jsx, SatelliteWaterQualityPanel.jsx

Empty / no-data states

ConditionMessageReference
No lakes at all"No lakes available"PortfolioPage.jsx
Filter/search yields nothing"No lakes match your filter"PortfolioPage.jsx
No bathymetry (2D) — null data or no mapCenter"No Bathymetry Data"Hydrography2D.jsx
No bathymetry (3D) — null volumeAndBathymetryData"Lake Data Not Found"Hydrography3D.jsx
No ASV points / error"No ASV Data" (+ error or "Select a date to load data.")AsvWaterQualityMap.jsx
No point trendspointTrendsError ?? 'No trend data available'PointTrendsPanel.jsx
No WQI score series"No WQI score data available"PointTrendsPanel.jsx
No dates in a pickerpicker returns null (hidden); filtered search "No dates found"HydrographyPanel.jsx
No lake imagesplaceholder mdi:waves icon blockPortfolioPage.jsx
No satellite params"No parameters configured."SatelliteWaterQualityPanel.jsx, WaterQualityPanel.jsx

Missing meshGrid / boundary

Hydrography2D guards if (!volumeAndBathymetryData?.meshGrid) return empty and requires non-empty x/y/z arrays before building depth points (Hydrography2D.jsx). The store's boundary computation only runs when x/y/z all have length and at least 3 valid lat/lng points exist (IlmStore.js); computeLakeBoundary itself returns [] for fewer than 3 points (IlmStore.js).

No track points

Point trends are conditional on bathymetry metadata. After bathymetry loads, selectLake only calls fetchPointTrends when meta.trackPoints is a non-empty array (IlmStore.js). WaterQualityPanel likewise gates PointTrendsPanel on hasTrackPoints && !isBaseline (WaterQualityPanel.jsx).

API errors / null handling

Every store fetch wraps its call in try/catch, logs a namespaced console.error, sets a slice-specific *Error string, and nulls its data (e.g. IlmStore.js). getLakeList failures are caught and leave lakes unchanged (empty), driving the "No lakes available" empty state. Satellite parsing tolerates array-or-object responses and only sets data when a record actually contains points (IlmStore.js).

Date / lake-selection boundaries

  • Selecting the currently selected lake is a no-op (selectLake early return, IlmStore.js).
  • On first date load, the first available date is auto-selected (satellite IlmStore.js; ASV IlmStore.js).
  • Baseline mode snaps to the last (oldest) date; Periodic mode snaps to the first date and always refetches (HydrographyPanel.jsx, WaterQualityPanel.jsx).
  • LakeDetailPage re-drives selection from the URL only when lakeId differs from selectedLakeUnitId and lakes are loaded (LakeDetailPage.jsx); a :lakeId with no matching lake yields a header fallback to the raw id (LakeHeader.jsx).

Multi-lake switching

selectLake clears every prior-lake slice before loading the new one, preventing stale bathymetry/quality/trend bleed-through across lakes (IlmStore.js).

Permission-denied / mode switches

  • Tabs the user lacks permission for are removed from the tab bar; Datatabs then auto-focuses the first unlocked tab (Datatabs.jsx).
  • The Water Availability tab can be present but locked (disabled, lock icon) when ILM_HIDE_HYDROGRAPHY is granted (Datatabs.jsx).
Offline / subscription-expired

The ILM library has no dedicated offline banner, navigator.onLine check, isDemo, or subscription-expired gate of its own. Offline/expired handling is inherited from shared app shell behaviour (SubPageWrapper, MainLayout, the splash validateLogin check) rather than implemented inside libs/ilm. Flag if product expects an ILM-specific offline state — none exists in code today.


Dependencies

Shared store & services

DependencyImportUsed for
apiClient@aquagen-mf-webapp/shared/servicesAll HTTP GETs (ilmDataSource.js)
Urls@aquagen-mf-webapp/shared/servicesEndpoint constants (ilmDataSource.js)
LocalDBInstance, LocalDBKeys@aquagen-mf-webapp/shared/servicesReads login response for satellite units (IlmStore.js)
AppStoreContext@aquagen-mf-webapp/shared/store/AppStoreloginData for permission checks (Datatabs.jsx)
PermissionController@aquagen-mf-webapp/shared/controller/permission/PermissionControllerTab gating (Datatabs.jsx)

Other AquaGen libs

LibImportUsed for
componentsSubPageWrapper (helper), FixedBar, CustomLoader (loader), If (logical)Page scaffold, sticky header, loaders, conditionals
gwiGwiPageGWI tab embed (Datatabs.jsx)
shared/helperLakePulseNavigationHelperStandalone nav helper

External npm libraries

LibraryComponent(s)Purpose
react-leaflet + leafletHydrography2D, AsvWaterQualityMap, LakeMapPreview, WaterRiskHeatMapMaps (depth points, ASV survey points, risk heat map, lake preview)
react-plotly.jsHydrography3D3D bathymetry surface plot
rechartsWaterAvailabilityTrendChart, PointTrendsPanelTrend line/area charts
@mui/material, @mui/icons-materialthroughoutLayout & icons
@iconify/reactthroughoutIcons
momentPortfolioPage, HydrographyPanel, PointTrendsPanelDate formatting/parsing

Controllers / data sources

  • IlmController (libs/ilm/src/controller/ilmController.js) — thin param-shaping layer over IlmDataSource.
  • IlmDataSource (libs/ilm/src/dataSource/ilmDataSource.js) — apiClient calls, console.error logging, re-throws on failure.

Visibility & Permissions

Which apps register the route

AppFileRegisters /lakepulse
productionapps/production/src/routes/routes.js
lakepulse (standalone)apps/lakepulse/src/routes/routes.js

The route element is not wrapped in a PermissionWrapper; access to the page itself is not gated at the route level. Gating happens on the sidebar entry and per tab.

Sidebar entry

YOUR_PORTFOLIO in LakePulseNavigationHelper.sideBarDetails (lakePulseNavigationHelperInstance.js):

YOUR_PORTFOLIO: {
access: SidebarAccess.OPEN, // always shown, never locked
displayName: 'Your Portfolio',
permissionId: 'ILM',
isDemoOption: false,
}

access: SidebarAccess.OPEN (libs/shared/src/enums/sidebarAccess.js) means the entry is not lock-gated. The permissionId: 'ILM' corresponds to Permissions.allPermission.ILM (libs/shared/src/enums/permissions.js).

Tab-level permissions (Datatabs)

Tab visibility and lock state are resolved via PermissionController.isPermitted(permissionId, appStore.loginData) (Datatabs.jsx):

TabpermissionId (show if granted)lockPermissionId (lock if granted)
Water Availability— (always visible)ILM_HIDE_HYDROGRAPHY
Water Quality IndexILM_WATER_QUALITY
Satellite Water QualityILM_SATELLITE_WATER_QUALITY
GWIGWI

A tab with a permissionId the user lacks is filtered out entirely; a tab whose lockPermissionId is granted stays visible but is disabled with a lock icon. SUPER_USER grants all ILM_* and GWI tabs (standard isPermitted super-user override — see Permissions).

The relevant permission enums (libs/shared/src/enums/permissions.js): ILM (14), GWI (13), ILM_WATER_QUALITY (35), ILM_SATELLITE_WATER_QUALITY (36), ILM_HIDE_HYDROGRAPHY (37).

Standalone store flags

standaloneAppName="lakepulse" sets activeWaterContext in AppStore (AppStore.js). There is no isFromLakepulse-style derived flag — feature code does not branch on the lakepulse context beyond nav-helper selection.

Demo / subscription gating

The ILM sidebar entry has isDemoOption: false, and libs/ilm contains no isDemo or subscription-expired checks. There is no ILM-specific demo path in code.


Usage Examples

1. Read ILM state from the store

import { useContext } from 'react';
import { IlmStoreContext } from '@aquagen-mf-webapp/ilm/store/IlmStore';

function LakeCount() {
const { lakes, lakesLoading, selectedLakeUnitId } = useContext(IlmStoreContext);
if (lakesLoading) return <span>Loading…</span>;
return <span>{lakes.length} lakes; selected: {selectedLakeUnitId ?? 'none'}</span>;
}

2. Switch the active lake

const { selectLake } = useContext(IlmStoreContext);
// Clears prior-lake data, then loads ASV dates, bathymetry, (conditional) point trends,
// and the water-availability trend for the new unit.
await selectLake('UNIT_LAKE_42');

3. Gate a custom control by tab permission

import { PermissionController } from '@aquagen-mf-webapp/shared/controller/permission/PermissionController';
import { AppStoreContext } from '@aquagen-mf-webapp/shared/store/AppStore';

const appStore = useContext(AppStoreContext);
const showSatellite = PermissionController.isPermitted(
'ILM_SATELLITE_WATER_QUALITY',
appStore.loginData
);


Last Updated: July 2026 Module Location: libs/ilm/src/