RO Plant Monitoring
Monitor Reverse Osmosis (RO) plant performance across multiple plants: recovery efficiency, membrane health score, permeate cost, running hours, flow (feed / permeate / reject), AI-style insights, offline alerts, and maintenance activity logging — with day / week / month / year / custom date ranges.
The feature is branded "RO Plant Monitoring Suite" in the UI header (libs/roEfficiency/src/RoEfficiencyPage.jsx) and "RO Plant" in the sidebar (libs/shared/src/helper/navHelperInstance.js). The module directory and code identifiers use the name roEfficiency.
Overview
The RO Plant module renders a multi-plant sidebar plus a detailed panel for the selected plant. The detail panel shows three metric tabs (Recovery, Health Score, Permeate Cost), a Recharts graph per tab, an AI Insight banner, a downloadable PNG report, and an Alerts / Log Activities section.
Location: libs/roEfficiency/src/
Route: /ro_efficiency
Permission Tag: RO_EFFICIENCY
Registered in apps: production and demo only (not rwi, uwms, or lakepulse).
Data source: Live backend API via apiClient (see Mock vs Live Data).
Key Features
1. Multi-Plant Sidebar
The sidebar lists every plant returned in roEfficiencyData.plants. Each card shows plant name, running status (Running / Not Running), run time (hours), Recovery %, and Health Score %, plus the top insight banner.
Source: libs/roEfficiency/src/components/RoEfficiencySidebar.jsx
Clicking a card sets selectedPlantIndex and fires the RO_EFFICIENCY_PLANT_SELECT analytics event.
2. Three Metric Tabs
Rendered by RoEfficiencyTabs (libs/roEfficiency/src/components/RoEfficiencyTabs.jsx). Each tab card shows a headline value, an info tooltip explaining the calculation, and supporting metrics:
| Tab | Headline value | Supporting metrics |
|---|---|---|
Recovery (efficiency) | plant.efficiency% | Total Feed, Total Permeate, Total Reject (from plant.flow.units matched by desc: ro_feed, ro_permeate, ro_reject) |
Health Score (health_score) | plant.healthScore% | One row per quality parameter (plant.quality.units[0].meta.params), using valueAvg or value |
Permeate Cost (cost) | ₹{plant.pricePerLitre}/{flowSiUnit} | Energy (plant.energy.units[0].value), Run time (plant.totalRunningHours) |
Conditional tab: The Permeate Cost tab is only shown when the plant object has an energy key — buildTabs() filters it out otherwise (RoEfficiencyTabs.jsx).
Recovery formula (from tooltip): (Permeate / Feed) × 100.
Permeate Cost formula (from tooltip): (Total Energy × Price) / Permeate.
Health Score: average of per-parameter scores across units, each parameter compared to its low/high thresholds and min/max range.
3. Graphs (Recharts)
All three graphs use recharts (ComposedChart, BarChart, LineChart). The visible graph follows the selected tab; during report download all graphs render simultaneously.
- Efficiency / Flow —
RoEfficiencyBarGraph.jsx(ComposedChart: Total Feed / Total Permeate bars + Efficiency line). Data fromplant.flow.formattedGraph. - Health Score —
RoEfficiencyHealthScoreGraph.jsx(LineChart). Data fromplant.quality.formattedGraph, params fromplant.quality.units[0].meta.params. - Cost —
RoEfficiencyCostGraph.jsx(BarChart+LineChart). Data fromplant.energy.formattedGraph, or granular unit data for theDAYtype.
4. AI Insight Banner
AiInsightsBanner (RoEfficiencyPlantDetails.jsx) plays a scripted loading sequence (RoEfficiencyHelper.LOADING_STEPS) then "types out" insight points derived from plant.insights via RoEfficiencyHelper.getInsightPoints. This is a client-side animation over data already present in plant.insights — no additional API call is made. If no insight points exist it falls back to a default "all parameters within expected ranges" message (RoEfficiencyPlantDetails.jsx).
5. Downloadable Report
The "Download Report" button expands all graphs (showAllGraphs) and captures the panel as a PNG via Download.PNG (@aquagen-mf-webapp/shared/utils/downloadUtil). Filename: {plantName}_{date1} with / replaced by -.
6. Alerts & Log Activities
RoEfficiencyAlerts.jsx provides a two-tab section:
- View Alerts — fetches general + offline alerts per unit via
AlertsDataSource.getAlertsByDateRange(alert_type: 'general_offline') for every unit inselectedPlant.units, over the date range fromRoEfficiencyHelper.getAlertDateRange. Clicking an alert navigates into the relevant monitoring page and marks it read. - Log Activities — a maintenance log form (activity type, date, optional notes) plus a chronological list of past activities. Activity types come from
RoEfficiencyHelper.ACTIVITY_TYPES(CIP Cleaning, Filter Replacement, Membrane Inspection, Chemical Dosing, General Inspection, Pump Maintenance, Other).
7. Date Range Selection
RoEfficiencyDatePicker.jsx offers Day, Week, Month, Year, Custom toggle buttons plus a date picker. Week maps to API type: 'CUSTOM' (RoEfficiencyHelper.RoEfficiencyDateType.WEEK.apiType); all others send their own id as type. The default on load is Month (RoEfficiencyStore.js).
Architecture
Data Flow
RoEfficiencyPage
↓ (wraps children in providers)
DashboardDataProvider → RoEfficiencyDataProvider (Context + polling)
↓
RoEfficiencyController.getRoEfficiencyData(params)
↓
RoEfficiencyDataSource.getRoEfficiencyData(params)
↓
apiClient.get(Urls.roEfficiencyData, params) → GET /roEfficiency/
↓
setRoEfficiencyData(response)
↓
BuildRoEfficiencyLayout → RoEfficiencySidebar + RoEfficiencyPlantDetails
Key Components
| Component | Location | Purpose |
|---|---|---|
RoEfficiencyPage | libs/roEfficiency/src/RoEfficiencyPage.jsx | Page shell: header, providers, SubPageWrapper, FixedBar |
RoEfficiencyDataProvider / RoEfficiencyDataContext | store/RoEfficiencyStore.js | State, polling, API orchestration |
RoEfficiencyController | controller/roEfficiencyController.js | Thin pass-through to the data source |
RoEfficiencyDataSource | dataSource/roEfficiencyDataSource.js | apiClient calls |
RoEfficiencyHelper | helper/roEfficiencyHelper.js | Date types, alert date ranges, insight/severity/label helpers, activity-type & alert-tab enums |
BuildRoEfficiencyLayout | components/BuildRoEfficiencyLayout.jsx | Loading / empty-state gate + two-column layout |
RoEfficiencySidebar | components/RoEfficiencySidebar.jsx | Plant selection cards |
RoEfficiencyPlantDetails | components/RoEfficiencyPlantDetails.jsx | Detail panel, AI insights, report download |
RoEfficiencyTabs | components/RoEfficiencyTabs.jsx | Recovery / Health Score / Cost tab cards |
RoEfficiencyBarGraph | components/RoEfficiencyBarGraph.jsx | Efficiency / flow ComposedChart |
RoEfficiencyHealthScoreGraph | components/RoEfficiencyHealthScoreGraph.jsx | Health score LineChart |
RoEfficiencyCostGraph | components/RoEfficiencyCostGraph.jsx | Energy / cost Bar + Line charts |
RoEfficiencyAlerts | components/RoEfficiencyAlerts.jsx | Alerts + Log Activities |
RoEfficiencyDatePicker | components/RoEfficiencyDatePicker.jsx | Date range controls |
Store State
Location: libs/roEfficiency/src/store/RoEfficiencyStore.js
| State | Purpose |
|---|---|
roEfficiencyData | Full API response ({ plants: [...] }) |
selectedPlant / selectedPlantIndex | Currently selected plant (falls back to plants[0]) |
isLoading | Main data load flag (initial true) |
selectedDateType / params | Date type + query params (date1, date2, type) |
logActivities / isFetchingActivities / isSubmittingActivity | Maintenance log state |
granularCostData / isGranularCostLoading | Day-level granular cost data |
Polling: init() runs on mount and re-runs whenever params changes, plus a setInterval at constants.refreshDuration (RoEfficiencyStore.js).
API Integration
Data source: libs/roEfficiency/src/dataSource/roEfficiencyDataSource.js
URL definitions: libs/shared/src/services/api/urls.js
| Method | HTTP | URL getter | Path |
|---|---|---|---|
getRoEfficiencyData(params) | GET | Urls.roEfficiencyData (urls.js) | /roEfficiency/ |
getLogActivities(params) | GET | Urls.getLogActivities (urls.js) | /roEfficiency/logActivities |
postLogActivity(body) | POST | Urls.postLogActivity (urls.js) | /roEfficiency/logActivities |
getGranularUnitData(params) | GET | Urls.getGranularUnitData (urls.js) | granular/unit |
Alerts are fetched separately from the shared alerts service — AlertsDataSource.getAlertsByDateRange (@aquagen-mf-webapp/shared/dataSource/alertsDataSource), not from a roEfficiency-specific endpoint.
GET /roEfficiency/ request params
Built from the store params:
{
date1: '01/07/2026', // DD/MM/YYYY, start of selected period
date2: '24/07/2026', // only for WEEK / CUSTOM
type: 'MONTH' // DAY | CUSTOM (for WEEK) | MONTH | YEAR | CUSTOM
}
Response shape (per plant)
The response is { plants: [ ... ] }. Each plant provides:
{
id: 'RO_EFFICIENCY_FLOW_1',
displayName: 'Ro Plant 1',
efficiency: 80.8, // recovery %
healthScore: 92.5, // may be null
pricePerLitre: 0, // permeate cost
totalRunningHours: 14, // running hours
runningStatus: true, // Running / Not Running
online: false,
units: ['DEMO2201F', ...], // unit ids, used for alert fetch
flow: { siUnit, units: [{ desc: 'ro_feed'|'ro_permeate'|'ro_reject', value }], formattedGraph: [...] },
quality: { units: [{ meta: { params: [...] }, value, valueAvg }], formattedGraph: [...] },
energy: { siUnit, units: [{ unitId, value }], formattedGraph: [...] }, // optional
insights: { shared: [...], efficiency: {...}, health: {...}, cost: {...} }
}
The exact field-level structure is mirrored in
libs/roEfficiency/src/dataSource/roEfficiencyMockData.js(see below), which the code comments describe as "the exact structure returned by the backend for RO Efficiency."
POST /roEfficiency/logActivities body
{
activityType: 'CIP_CLEANING', // key from RoEfficiencyHelper.ACTIVITY_TYPES
description: 'Cleaned membranes',
date: '24/07/2026'
}
Mock vs Live Data
The module runs on the live backend API. Every store call goes through apiClient (roEfficiencyDataSource.js).
A file libs/roEfficiency/src/dataSource/roEfficiencyMockData.js exists and exports RO_EFFICIENCY_MOCK_DATA (418 lines), but it is not imported anywhere in the codebase (verified across libs/ and apps/). It is dead code retained as a structural reference. Its header comment reads:
// TODO: Remove this file once the backend API is live.
// This is the exact structure returned by the backend for RO Efficiency.
Documentation implication: Do not describe RO Plant as mock-driven. The mock file is useful only as a reference for the expected response shape; the runtime path is fully live.
Edge Cases
Loading state
BuildRoEfficiencyLayout shows a centered CustomLoader (80vh) while isLoading is true (BuildRoEfficiencyLayout.jsx). isLoading starts true and toggles in init() (RoEfficiencyStore.js). The date-picker toggle/date controls are disabled while loading (RoEfficiencyDatePicker.jsx).
Empty / no-data
If the response has no plants key, the layout renders <PageNotFound /> (BuildRoEfficiencyLayout.jsx). Note the guard is on roEfficiencyData?.plants (truthiness of the key), so an empty array plants: [] passes the guard and renders an empty sidebar.
API error / null response
Read calls (getRoEfficiencyData, getLogActivities, getGranularUnitData) wrap apiClient in try/catch, log to console.error, and return undefined on failure (roEfficiencyDataSource.js). In the store, init() only calls setRoEfficiencyData when the response is truthy, so a failed refresh keeps the previous data. On the very first load, a failed/empty response leaves roEfficiencyData undefined → PageNotFound.
Null field handling
The UI is defensive throughout:
- Recovery/Health Score show
'Not Available'whenefficiency/healthScoreare null (RoEfficiencySidebar.jsx). totalRunningHoursfalls back to0;runningStatusfalsy → "Not Running" in red.- Tab metric values render
'--'when null (RoEfficiencyTabs.jsx), and headline cost shows'--'whenpricePerLitreis null. - Graph data uses
?? []fallbacks (RoEfficiencyPlantDetails.jsx).
Offline plant
plant.online and plant.runningStatus are surfaced separately. Cards display "Not Running" (red) when runningStatus is falsy; graphs render whatever formattedGraph/efficiencyGraph contains (mock data includes offlineCount / offlineDuration / offlineIntervals per bucket for offline visualization).
Permission-denied
There is no route-level permission gate (see Visibility Parameters) — direct navigation to /ro_efficiency renders the page regardless of the RO_EFFICIENCY permission. Gating happens at the sidebar level: users without the permission see the entry as locked and are routed to the feature-locked marketing page instead of the live module.
Subscription-expired / feature-locked
When locked, the Modules section's EXPLORE_BUTTON lock style routes to /feature_locked/MODULES, which renders the RO marketing card (lockedFeatureHelper.js, displayName: 'RO Plant Module'). This is the "explore / find out more" experience rather than the functional page.
Date-range boundaries
RoEfficiencyHelper.getAlertDateRange (roEfficiencyHelper.js) clamps MONTH/YEAR end dates to today when the selected period is the current month/year (so it never queries future dates), and uses full month/year boundaries for past periods. WEEK/CUSTOM use the explicit date1–date2 pair.
Plant selection (multi-plant)
selectedPlant is derived as plants[selectedPlantIndex] ?? plants[0] (RoEfficiencyStore.js), so an out-of-range index safely falls back to the first plant. Switching plants re-triggers the alerts fetch (its effect depends on selectedPlant, RoEfficiencyAlerts.jsx).
Log-activity add/edit states
- Submit is disabled while
isSubmittingActivityistrueor noactivityTypeis selected (RoEfficiencyAlerts.jsx). - On submit,
postLogActivitysets a submitting flag, posts, then re-fetches the list; a success/failureActivityStatusModalis shown; on success the form is reset (RoEfficiencyAlerts.jsx,RoEfficiencyStore.js). - Post errors are caught and surfaced as
{ success: false }→ failure modal (no throw to the user). - There is no edit/delete of existing activities — the log is append-only and sorted newest-first by
createdAt(RoEfficiencyStore.js).
Alerts empty / loading state
While fetching, a CustomLoader shows; when the result is empty, a GenericInfo "No alerts found for this unit" empty state with an empty-notification Lottie is shown (RoEfficiencyAlerts.jsx). The Log Activities list has its own "No activities logged yet" empty state (RoEfficiencyAlerts.jsx).
Granular (Day) cost data
The Cost tab fetches granular unit data only when selectedDateType === 'DAY' and an energyUnitId exists (RoEfficiencyPlantDetails.jsx). fetchGranularCostData no-ops without unitId/date1, shows a spinner via isGranularCostLoading, and falls back to plant.energy.formattedGraph when no granular data is returned.
Dependencies
Shared store & services
AppStoreContext(@aquagen-mf-webapp/shared/store/AppStore) —RoEfficiencyPagesetsmenuIconEnabled(false)andselectedCategoryon mount (RoEfficiencyPage.jsx).apiClientandUrls(@aquagen-mf-webapp/shared/services) — all RO data calls.AnalyticsService+AnalyticEvents(@aquagen-mf-webapp/shared/enums) — page-view and RO-specific events (see Analytics).constants(@aquagen-mf-webapp/shared/constants) —refreshDurationfor polling.AlertsDataSourceandAlertsController(shared) — alert fetch and read-marking.NavigationHelper(@aquagen-mf-webapp/shared/helper) anduseNavigateSearchParamshook — alert click-through navigation and "Account Settings" deep link.Downloadutil (@aquagen-mf-webapp/shared/utils/downloadUtil) — PNG report export.StandardCategoryTypeUppercaseandDatepickerEnum(shared enums) — alert routing and date-picker types.
Other AquaGen libs
@aquagen-mf-webapp/dashboard—DashboardDataProviderwraps the page (RoEfficiencyPage.jsx).@aquagen-mf-webapp/components—FixedBar,SubPageWrapper/Expanded(helper),CustomLoader/BackdropLoader(loader),AppButton,AppDatePickerSelection,SearchComponent,PageNotFound/GenericInfo(info),AppTooltip(popover).
External npm libraries
recharts— all three graphs (ComposedChart,BarChart,LineChart, etc.). (No chart.js in this module.)@mui/material+@mui/icons-material— layout and controls.@iconify/react— icons.moment— date formatting/boundaries (store, helper, components).lodash(has,_.map) — tab gating and date-type iteration.
Controllers / data sources
RoEfficiencyController → RoEfficiencyDataSource → apiClient. The controller is a straight pass-through (controller/roEfficiencyController.js).
Navigation helper
NavigationHelper (navHelperInstance.js) registers the /ro_efficiency route (:76), the sidebar entry (:307-319), and its placement in the Modules section (:117-127).
Visibility Parameters
Which apps register the route
The route is registered only in:
apps/production/src/routes/routes.jsapps/demo/src/routes/routes.js
Both register it as a child of the root layout:
{
path: 'ro_efficiency',
element: <RoEfficiencyPage />,
}
It is not registered in rwi, uwms, or lakepulse.
Exact permission tag & gating
- Permission tag:
RO_EFFICIENCY— defined inlibs/shared/src/enums/permissions.js(RO_EFFICIENCY: 'RO_EFFICIENCY'). This is a dedicated tag, distinct fromEFFICIENCY/EFFICENCY(permissions.js). - Route element is NOT wrapped in
PermissionWrapper. Unlike some routes,/ro_efficiencyrenders<RoEfficiencyPage />directly. There is noPermissionController.isPermitted/PermissionWrappercheck insideRoEfficiencyPageeither. Access control is therefore enforced at the sidebar/navigation layer, not at the route. A user who navigates directly to the URL will load the page.
Sidebar lock state
- Sidebar entry:
sideBarDetails.RO_EFFICIENCY(navHelperInstance.js) withpermissionId: 'RO_EFFICIENCY'and no explicitaccessfield, so it uses the defaultSidebarAccess.LOCKED— "show as locked if the user lacks the permission; super users bypass" (navHelperInstance.js, enum inlibs/shared/src/enums/sidebarAccess.js). - Section:
RO_EFFICIENCYsits in themodulesnav section (navHelperInstance.js), alongsideUWI,ILM,RAIN_WATER. That section declareslockStyle: SidebarLockStyle.EXPLORE_BUTTONandlockedPath: '/feature_locked/MODULES', so a locked RO entry presents an "Explore" affordance leading to the marketing page.
Super-user behaviour
Because the sidebar entry uses default LOCKED (not STRICT/STRICT_HIDDEN), super users bypass the check and see RO Plant as unlocked, per the documented SidebarAccess semantics (navHelperInstance.js). The route has no ignoreSuperUser gate.
Feature-locked page behaviour
The locked/marketing experience is defined in libs/featureLocked/src/appNavHelpers/lockedFeatureHelper.js:
{
id: 'RO',
permissionId: 'RO_EFFICIENCY',
displayName: 'RO Plant Module',
enabledFeature: true,
description: 'Analyze RO plant performance, recovery efficiency, and operational health.',
ctaLabel: 'Find out more about this product',
featureDetails: [ /* recovery optimization, membrane health, energy per litre, maintenance scheduling */ ]
}
isDemo / subscription gating
The sidebar entry sets isDemoOption: false (navHelperInstance.js), meaning it is not demo-only and is not removed when constants.isDemo is false (per the isDemoOption rule at navHelperInstance.js). It appears in both the production and demo builds that register the route. There is no separate subscription-expiry check inside the module — "subscription/premium" gating is expressed through the locked-module + feature-locked marketing flow described above.
Analytics Integration
Events are defined in libs/shared/src/enums/analyticsEnum.js and dispatched via AnalyticsService.sendEvent:
| Event | Fired from |
|---|---|
PAGE_VIEW | RoEfficiencyPage mount (RoEfficiencyPage.jsx) |
RO_EFFICIENCY_DATE_CHANGE | Date type change (RoEfficiencyDatePicker.jsx) |
RO_EFFICIENCY_PLANT_SELECT | Plant card click (RoEfficiencySidebar.jsx) |
RO_EFFICIENCY_TAB_CHANGE | Metric tab change (RoEfficiencyTabs.jsx) |
RO_EFFICIENCY_INSIGHTS_TOGGLE | AI Insights open/close (RoEfficiencyPlantDetails.jsx) |
RO_EFFICIENCY_DOWNLOAD_REPORT | Download report (RoEfficiencyPlantDetails.jsx) |
RO_EFFICIENCY_LOG_ACTIVITY | Log activity submit (RoEfficiencyAlerts.jsx) |
RO_EFFICIENCY_ALERT_TAB_CHANGE | Alerts/Log tab toggle (RoEfficiencyAlerts.jsx) |
Best Practices
Guard on plant fields, not just the plant
Recovery, Health Score, and cost can each be null independently. Use the existing null checks ('Not Available' / '--') rather than assuming a plant always has all metrics.
Respect the conditional Cost tab
Only render cost logic when plant.energy exists — buildTabs already filters the Cost tab out otherwise (RoEfficiencyTabs.jsx).
Keep the mock file as reference only
roEfficiencyMockData.js documents the response shape but must not be imported into runtime code; it is slated for removal once the backend is confirmed live.
Related Documentation
- Permissions & Access Control -
RO_EFFICIENCYpermission and sidebar locking - Routes - Route registration across apps
- Alerts - Shared alerts service used by RO Plant
- Water Quality Monitoring - Related parameter/threshold model
- Energy Monitoring - Energy data feeding permeate cost
- API & Services -
apiClientandUrls
Last Updated: July 2026
Module Location: libs/roEfficiency/src/