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
productionapp at/lakepulse. - As the dedicated
lakepulsestandalone 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>
);
}
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 === selectedLakeUnitIdreturns early). - Clears all stale data from the previous lake (satellite, ASV, boundary, bathymetry, point trends, water-availability trend).
- Sets
selectedLakeUnitId,bathymetryUnitId, andasvUnitId. - Matches a satellite unit by index against
satelliteUnits(loaded from the login response'sID_SATELLITE_LAKEcategory), falling back tosatelliteUnits[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).
5. Point trends (turbidity / TSS / WQI)
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
lakeBoundaryfrom the bathymetrymeshGrid(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 action | Controller | Urls key | Endpoint | Params |
|---|---|---|---|---|
getLakeList() | getLakeList | lakeList | wri/lakeList | — |
calculateVolumeAndBathymetry() | calculateVolumeAndBathymetry | calculateVolumeAndBathymetry | wri/bathymetryResult | { unitId, date? } |
getHydrographyData() | getHydrographyData | categoryDataV2 | deviceDataV2 | params |
fetchAvailableDates() | getDatesList | wriDatesList | wri/datesList | { unitId } |
fetchSatelliteData() | getSatelliteData | wriSatelliteData | wri/satelliteData | { date, unitId } |
fetchAsvDates() | getAsvDates | asvDates | wri/asv/dates | { unitId } |
fetchAsvQuality() | getAsvQuality | asvQuality | wri/asv/quality | { unitId, date } |
fetchPointTrends() | getPointTrends | ilmPointTrends | wri/ilm/pointTrends | { unitId } |
fetchWaTrend() | getTrendWaterAvailability | ilmTrendWaterAvailability | wri/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
| Slice | Loading UI | Reference |
|---|---|---|
| Lake list (portfolio) | 6 Skeleton cards | PortfolioPage.jsx |
| Bathymetry 3D | CustomLoader (centered) | Hydrography3D.jsx |
| Bathymetry 2D | CircularProgress | Hydrography2D.jsx |
| ASV map | CircularProgress | AsvWaterQualityMap.jsx |
| Point trends | "Loading trend data…" | PointTrendsPanel.jsx |
| Water-availability trend | spinner | WaterAvailabilityTrendChart.jsx |
| Date pickers | "Loading..." label | HydrographyPanel.jsx, WaterQualityPanel.jsx, SatelliteWaterQualityPanel.jsx |
Empty / no-data states
| Condition | Message | Reference |
|---|---|---|
| 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 trends | pointTrendsError ?? 'No trend data available' | PointTrendsPanel.jsx |
| No WQI score series | "No WQI score data available" | PointTrendsPanel.jsx |
| No dates in a picker | picker returns null (hidden); filtered search "No dates found" | HydrographyPanel.jsx |
| No lake images | placeholder mdi:waves icon block | PortfolioPage.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 (
selectLakeearly return,IlmStore.js). - On first date load, the first available date is auto-selected (satellite
IlmStore.js; ASVIlmStore.js). - Baseline mode snaps to the last (oldest) date; Periodic mode snaps to the first date and always refetches (
HydrographyPanel.jsx,WaterQualityPanel.jsx). LakeDetailPagere-drives selection from the URL only whenlakeIddiffers fromselectedLakeUnitIdand lakes are loaded (LakeDetailPage.jsx); a:lakeIdwith 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;
Datatabsthen auto-focuses the first unlocked tab (Datatabs.jsx). - The Water Availability tab can be present but locked (disabled, lock icon) when
ILM_HIDE_HYDROGRAPHYis granted (Datatabs.jsx).
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
| Dependency | Import | Used for |
|---|---|---|
apiClient | @aquagen-mf-webapp/shared/services | All HTTP GETs (ilmDataSource.js) |
Urls | @aquagen-mf-webapp/shared/services | Endpoint constants (ilmDataSource.js) |
LocalDBInstance, LocalDBKeys | @aquagen-mf-webapp/shared/services | Reads login response for satellite units (IlmStore.js) |
AppStoreContext | @aquagen-mf-webapp/shared/store/AppStore | loginData for permission checks (Datatabs.jsx) |
PermissionController | @aquagen-mf-webapp/shared/controller/permission/PermissionController | Tab gating (Datatabs.jsx) |
Other AquaGen libs
| Lib | Import | Used for |
|---|---|---|
components | SubPageWrapper (helper), FixedBar, CustomLoader (loader), If (logical) | Page scaffold, sticky header, loaders, conditionals |
gwi | GwiPage | GWI tab embed (Datatabs.jsx) |
shared/helper | LakePulseNavigationHelper | Standalone nav helper |
External npm libraries
| Library | Component(s) | Purpose |
|---|---|---|
react-leaflet + leaflet | Hydrography2D, AsvWaterQualityMap, LakeMapPreview, WaterRiskHeatMap | Maps (depth points, ASV survey points, risk heat map, lake preview) |
react-plotly.js | Hydrography3D | 3D bathymetry surface plot |
recharts | WaterAvailabilityTrendChart, PointTrendsPanel | Trend line/area charts |
@mui/material, @mui/icons-material | throughout | Layout & icons |
@iconify/react | throughout | Icons |
moment | PortfolioPage, HydrographyPanel, PointTrendsPanel | Date formatting/parsing |
Controllers / data sources
IlmController(libs/ilm/src/controller/ilmController.js) — thin param-shaping layer overIlmDataSource.IlmDataSource(libs/ilm/src/dataSource/ilmDataSource.js) —apiClientcalls,console.errorlogging, re-throws on failure.
Visibility & Permissions
Which apps register the route
| App | File | Registers /lakepulse |
|---|---|---|
production | apps/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):
| Tab | permissionId (show if granted) | lockPermissionId (lock if granted) |
|---|---|---|
| Water Availability | — (always visible) | ILM_HIDE_HYDROGRAPHY |
| Water Quality Index | ILM_WATER_QUALITY | — |
| Satellite Water Quality | ILM_SATELLITE_WATER_QUALITY | — |
| GWI | GWI | — |
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
);
Related Documentation
- Standalone Apps - The bootstrap / navInstance / standaloneAppName pattern Lake Pulse follows
- Applications Overview - Lakepulse app / ilm lib in the monorepo
- Permissions - PermissionController, isPermitted, super-user rules
- Water Quality Monitoring - Sibling water-quality feature
- Ground Water Level - GWI, embedded as a Lake Pulse tab
- API & Services - apiClient and Urls
Last Updated: July 2026
Module Location: libs/ilm/src/