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 IlmController → IlmDataSource → apiClient.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/