Skip to main content

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:

TabHeadline valueSupporting 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 / FlowRoEfficiencyBarGraph.jsx (ComposedChart: Total Feed / Total Permeate bars + Efficiency line). Data from plant.flow.formattedGraph.
  • Health ScoreRoEfficiencyHealthScoreGraph.jsx (LineChart). Data from plant.quality.formattedGraph, params from plant.quality.units[0].meta.params.
  • CostRoEfficiencyCostGraph.jsx (BarChart + LineChart). Data from plant.energy.formattedGraph, or granular unit data for the DAY type.

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 in selectedPlant.units, over the date range from RoEfficiencyHelper.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

ComponentLocationPurpose
RoEfficiencyPagelibs/roEfficiency/src/RoEfficiencyPage.jsxPage shell: header, providers, SubPageWrapper, FixedBar
RoEfficiencyDataProvider / RoEfficiencyDataContextstore/RoEfficiencyStore.jsState, polling, API orchestration
RoEfficiencyControllercontroller/roEfficiencyController.jsThin pass-through to the data source
RoEfficiencyDataSourcedataSource/roEfficiencyDataSource.jsapiClient calls
RoEfficiencyHelperhelper/roEfficiencyHelper.jsDate types, alert date ranges, insight/severity/label helpers, activity-type & alert-tab enums
BuildRoEfficiencyLayoutcomponents/BuildRoEfficiencyLayout.jsxLoading / empty-state gate + two-column layout
RoEfficiencySidebarcomponents/RoEfficiencySidebar.jsxPlant selection cards
RoEfficiencyPlantDetailscomponents/RoEfficiencyPlantDetails.jsxDetail panel, AI insights, report download
RoEfficiencyTabscomponents/RoEfficiencyTabs.jsxRecovery / Health Score / Cost tab cards
RoEfficiencyBarGraphcomponents/RoEfficiencyBarGraph.jsxEfficiency / flow ComposedChart
RoEfficiencyHealthScoreGraphcomponents/RoEfficiencyHealthScoreGraph.jsxHealth score LineChart
RoEfficiencyCostGraphcomponents/RoEfficiencyCostGraph.jsxEnergy / cost Bar + Line charts
RoEfficiencyAlertscomponents/RoEfficiencyAlerts.jsxAlerts + Log Activities
RoEfficiencyDatePickercomponents/RoEfficiencyDatePicker.jsxDate range controls

Store State

Location: libs/roEfficiency/src/store/RoEfficiencyStore.js

StatePurpose
roEfficiencyDataFull API response ({ plants: [...] })
selectedPlant / selectedPlantIndexCurrently selected plant (falls back to plants[0])
isLoadingMain data load flag (initial true)
selectedDateType / paramsDate type + query params (date1, date2, type)
logActivities / isFetchingActivities / isSubmittingActivityMaintenance log state
granularCostData / isGranularCostLoadingDay-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

MethodHTTPURL getterPath
getRoEfficiencyData(params)GETUrls.roEfficiencyData (urls.js)/roEfficiency/
getLogActivities(params)GETUrls.getLogActivities (urls.js)/roEfficiency/logActivities
postLogActivity(body)POSTUrls.postLogActivity (urls.js)/roEfficiency/logActivities
getGranularUnitData(params)GETUrls.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' when efficiency / healthScore are null (RoEfficiencySidebar.jsx).
  • totalRunningHours falls back to 0; runningStatus falsy → "Not Running" in red.
  • Tab metric values render '--' when null (RoEfficiencyTabs.jsx), and headline cost shows '--' when pricePerLitre is 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 date1date2 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 isSubmittingActivity is true or no activityType is selected (RoEfficiencyAlerts.jsx).
  • On submit, postLogActivity sets a submitting flag, posts, then re-fetches the list; a success/failure ActivityStatusModal is 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) — RoEfficiencyPage sets menuIconEnabled(false) and selectedCategory on mount (RoEfficiencyPage.jsx).
  • apiClient and Urls (@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) — refreshDuration for polling.
  • AlertsDataSource and AlertsController (shared) — alert fetch and read-marking.
  • NavigationHelper (@aquagen-mf-webapp/shared/helper) and useNavigateSearchParams hook — alert click-through navigation and "Account Settings" deep link.
  • Download util (@aquagen-mf-webapp/shared/utils/downloadUtil) — PNG report export.
  • StandardCategoryTypeUppercase and DatepickerEnum (shared enums) — alert routing and date-picker types.

Other AquaGen libs

  • @aquagen-mf-webapp/dashboardDashboardDataProvider wraps the page (RoEfficiencyPage.jsx).
  • @aquagen-mf-webapp/componentsFixedBar, 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

RoEfficiencyControllerRoEfficiencyDataSourceapiClient. The controller is a straight pass-through (controller/roEfficiencyController.js).

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.js
  • apps/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 in libs/shared/src/enums/permissions.js (RO_EFFICIENCY: 'RO_EFFICIENCY'). This is a dedicated tag, distinct from EFFICIENCY / EFFICENCY (permissions.js).
  • Route element is NOT wrapped in PermissionWrapper. Unlike some routes, /ro_efficiency renders <RoEfficiencyPage /> directly. There is no PermissionController.isPermitted / PermissionWrapper check inside RoEfficiencyPage either. 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 entry: sideBarDetails.RO_EFFICIENCY (navHelperInstance.js) with permissionId: 'RO_EFFICIENCY' and no explicit access field, so it uses the default SidebarAccess.LOCKED — "show as locked if the user lacks the permission; super users bypass" (navHelperInstance.js, enum in libs/shared/src/enums/sidebarAccess.js).
  • Section: RO_EFFICIENCY sits in the modules nav section (navHelperInstance.js), alongside UWI, ILM, RAIN_WATER. That section declares lockStyle: SidebarLockStyle.EXPLORE_BUTTON and lockedPath: '/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:

EventFired from
PAGE_VIEWRoEfficiencyPage mount (RoEfficiencyPage.jsx)
RO_EFFICIENCY_DATE_CHANGEDate type change (RoEfficiencyDatePicker.jsx)
RO_EFFICIENCY_PLANT_SELECTPlant card click (RoEfficiencySidebar.jsx)
RO_EFFICIENCY_TAB_CHANGEMetric tab change (RoEfficiencyTabs.jsx)
RO_EFFICIENCY_INSIGHTS_TOGGLEAI Insights open/close (RoEfficiencyPlantDetails.jsx)
RO_EFFICIENCY_DOWNLOAD_REPORTDownload report (RoEfficiencyPlantDetails.jsx)
RO_EFFICIENCY_LOG_ACTIVITYLog activity submit (RoEfficiencyAlerts.jsx)
RO_EFFICIENCY_ALERT_TAB_CHANGEAlerts/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.



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