STP / AquaRecycle (UWI)
Monitor, manage and optimise a Sewage Treatment Plant (STP) through the Used Water Index (UWI) module: a live plant view with remote control, per-unit STP energy consumption and savings analytics, and CO₂-reduction tracking. The module is the entire product surface of the standalone AquaRecycle (uwms) app, and is also mounted inside the main production app.
Overview
The UWI module (internally "Used Water Index"; product name AquaRecycle) wraps the uwi feature library around a shared SCADA plant diagram, water-monitoring pages, and an energy analytics dashboard. It presents two tabs under a common layout — Plant View and STP Energy — backed by a single dashboard store.
Location: libs/uwi/src/
Route: /uwi (redirects to /uwi/plant-view)
Permission Required: UWI (sidebar entries use SidebarAccess.OPEN in the standalone app, so they are always visible there — see Visibility & Permissions)
Registering apps:
uwms(AquaRecycle, standalone) —apps/uwms/src/routes/routes.jsproduction(main app) —apps/production/src/routes/routes.js
Both apps register the same three UWI routes. The main app additionally exposes the standard catalog (energy, water balance, RWI, SCADA, etc.); the standalone app trims the route table to the STP vertical plus the monitoring/alerts/reports pages that Plant View links to.
Key Features
1. Plant View (/uwi/plant-view)
The operational home screen for the STP:
- Stat cards (
UWMSPlantViewStatsCard) — Current Water Quality (safe/unsafe device counts), Water Treated Today (kL), Energy Consumed Today (kWh), and Total Energy Saved (kWh, permission-gated). Each card deep-links to the relevant page. - Live SCADA diagram — the shared
ScadaPageViewer(page"main") rendered withshowControlPanel={true}, which surfaces the STP Remote Control panel and Remote Control Logs. - Cross-module navigation buttons — "View the STP's Water Flow / Water Stock / Water Quality" (rendered only outside the standalone app; the standalone app already exposes these in its sidebar).
Location: libs/uwi/src/components/UwiPlantViewPage.jsx
2. STP Energy (/uwi/uwms-energy)
Energy consumption, savings and runtime analytics per sub-category and unit:
- Energy & Carbon cards (
EnergyCarbonCards) — energy saved and CO₂ reduced, with a "Since project began" toggle. Gated byUWI_SHOW_ENERGY_SAVING. - Cumulative split graph (
UwiGraphSection) — per-unit cumulative energy over the selected period. - Time & unit filters (
UwiDashboardFilters) — Day / Week / Month / Year / Custom selection and per-unit toggles. - Unit-wise energy details (
UnitWiseEnergyDetails) — a table of energy consumed and runtime per unit.
Location: libs/uwi/src/components/UwiEnergyPage.jsx
3. Remote Control & Control Logs
Because Plant View mounts ScadaPageViewer with showControlPanel, operators can issue writes to plant registers (via the SCADA RemoteControlPanel → ScadaController.writeRegisters) and review a dated audit of Remote Control Logs. The UWI data layer also exposes a parallel control-logs fetch (getUwmsControlLogs) returning a normalized array.
4. Energy Saved & CO₂ Reduction
Energy-saved figures are aggregated for month / year / all-time windows and converted to CO₂ using a fixed factor:
const CO2_FACTOR = 0.79; // libs/uwi/src/controller/uwiController.js
const toCo2 = (energy) =>
energy || energy === 0 ? parseFloat(energy) * CO2_FACTOR : null;
// Formula shown in UI: "CO₂ = Energy Saved * 0.79"
Architecture
Data Flow
UwiLayoutPage (route: /uwi)
↓ provides
UwiDashboardStoreContextProvider (libs/uwi/src/store/UwiStore.js)
↓ calls
UwiDashboardController (libs/uwi/src/controller/uwiController.js)
↓ calls
UwiDashboardDataSource (libs/uwi/src/dataSource/uwiDataSource.js)
↓ apiClient.get(Urls.*)
Backend (main API + uwiAI backend)
↓
Store state → UwiPageShell → <Outlet/> → PlantViewPage | UwmsEnergyPage
The layout also fires session analytics via useUwiSessionAnalytics (PAGE_VIEW on mount, UWI_DASHBOARD_SESSION_TIME with duration on unmount).
Key Components
1. UwiLayoutPage
- Purpose: Route layout for
/uwi; installs the store, page shell and analytics. - Location:
libs/uwi/src/UwiLayoutPage.jsx - Renders
UwiDashboardStoreContextProvider > UwiPageShell > <Outlet/>.
2. UwiPageShell
- Purpose: Header/title chrome for each tab plus the search box.
- Location:
libs/uwi/src/components/UwiPageShell.jsx - Header map:
plant-view→ "Plant View" / "Monitor, Manage and Optimise the Sewage Treatment Plant";uwms-energy→ "STP Energy" / "Monitor Energy consumption, Savings and Runtime". Default title "Sewage Treatment Plant". Theuwms-energyroute uses body scroll; Plant View uses a fixed 100vh layout.
3. UwiPlantViewPage (exported as PlantViewPage)
- Location:
libs/uwi/src/components/UwiPlantViewPage.jsx - Reads
standaloneAppNamefromAppStoreContext;isUwmsNativeApp = appStore?.standaloneAppName === 'uwms'. - In the main app it calls
appStore.setWaterContext('uwms')before navigating so the shared store adopts the UWMS context.
4. UwiEnergyPage (exported as UwmsEnergyPage)
- Location:
libs/uwi/src/components/UwiEnergyPage.jsx - Composes
EnergyCarbonCards,UwiGraphSection,UwiDashboardFilters,UnitWiseEnergyDetails.
5. UWMSPlantViewStatsCard
- Location:
libs/uwi/src/components/UWMSPlantViewStatsCard.jsx - Consumes
uwmsStatsfrom the store; renders four responsive stat cards.
6. UwiDashboardStoreContextProvider
- Purpose: Single source of truth for date/range/type selection, dashboard data, energy-saved & CO₂ stats, and plant stats.
- Location:
libs/uwi/src/store/UwiStore.js
Supporting components: EnergyCarbonCards.jsx, UnitWiseEnergyDetails.jsx, UwiDashboardFilters.jsx, UwiDashboardDatePicker.jsx, UwiMonthPickerField.jsx, UwiGraphSection.jsx, and graphs graph/UwiBarGraph.jsx / graph/UwiLineGraph.jsx (which wrap the shared components/graph/LineGraph).
API Integration
All calls go through the shared apiClient with endpoints from libs/shared/src/services/api/urls.js.
Code Reference: libs/uwi/src/dataSource/uwiDataSource.js
| Data source method | Urls getter | Path | Used for |
|---|---|---|---|
getUwiDashboardData(params) | Urls.getUwidashboardData | /deviceDataV2 | Main STP Energy dashboard data (sub-categories, units, graphs) |
getEnergySavedAndCO2Reduced(params) | Urls.categoryData | deviceData/ | Energy saved & CO₂ (ENERGY_CATEGORY / subCategory UWI) |
getEnergyConsumedBySubCategoryAndUnits(params) | Urls.categoryDataV2 | deviceDataV2 | Energy consumed by sub-category and units |
getUwmsStats(params) | Urls.uwmsStats | uwms/stats | Plant View stat cards |
getUwmsControlLogs(date) | Urls.uwmsControlLogs | uwms/control-logs | Remote control audit logs |
uwiAI backend: the module has a dedicated AI backend base URL, environment-switched:
// libs/shared/src/services/api/urls.js
get uwiAIBaseUrl() {
switch (constants.currentEnv) {
case constants.env.local:
return 'http://localhost:3001/';
default:
return 'https://uwi-ai-backend-guf8fsdyb4e7gyh2.centralindia-01.azurewebsites.net/';
}
}
Request parameters
buildUwiParams normalizes the query for the main dashboard call:
// libs/uwi/src/helper/uwiHelper.js
const buildUwiParams = ({ date1, type, category = 'UWI', extra = {} }) => ({
date1,
category,
type: type?.toUpperCase(), // DAY | WEEK | MONTH | YEAR | CUSTOM
...extra,
});
For CUSTOM/week ranges the store sends { type: 'CUSTOM', category: 'UWI', date1, date2 } (see UwiStore.js). Dates are DD/MM/YYYY (moment-formatted).
Usage Examples
1. Consume the UWI store in a component
import { useUwiDashboardStore } from '@aquagen-mf-webapp/uwi/store/UwiStore';
function EnergyHeadline() {
const { uwmsStats, uwmsStatsLoading, uwiDashboardData } = useUwiDashboardStore();
if (uwmsStatsLoading && !uwmsStats) return 'Loading…';
return `Energy consumed today: ${uwmsStats?.energy_consumed_today ?? '--'} kWh`;
}
2. Fetch dashboard data for a custom range
import { UwiDashboardController } from '@aquagen-mf-webapp/uwi/controller/uwiController';
import { buildUwiParams } from '@aquagen-mf-webapp/uwi/helper/uwiHelper';
const data = await UwiDashboardController.getUwiDashboardData(
buildUwiParams({ date1: '01/07/2026', type: 'month' })
);
3. Gate the energy-savings UI
import { PermissionWrapper } from '@aquagen-mf-webapp/components/permissionWrapper';
import { Permissions } from '@aquagen-mf-webapp/shared/enums';
<PermissionWrapper tag={Permissions.allPermission.UWI_SHOW_ENERGY_SAVING}>
<EnergyCarbonCards />
</PermissionWrapper>
Code Reference: libs/uwi/src/components/UwiEnergyPage.jsx
Standalone App (uwms / AquaRecycle)
AquaRecycle follows the current standalone-app pattern (see Standalone Apps). Its three "standalone ingredients":
1. Per-app login check — createValidateLogin(UwmsNavigationHelper), created once before render:
// apps/uwms/src/bootstrap.jsx
import { createValidateLogin } from '@aquagen-mf-webapp/components/pages/splash/splashController';
import { UwmsNavigationHelper } from '@aquagen-mf-webapp/shared/helper/uwmsNavigationHelperInstance';
const validateLogin = createValidateLogin(UwmsNavigationHelper);
2. Per-app navigation helper — passed into the layout so redirects use the STP route table:
// apps/uwms/src/routes/routes.js
<MainLayout navInstance={UwmsNavigationHelper} />
3. standaloneAppName="uwms" — passed into the shared store:
// apps/uwms/src/bootstrap.jsx
<AppStoreContextProvider validateLogin={validateLogin} standaloneAppName="uwms">
Store flags derived from this (libs/shared/src/store/AppStore.js): activeWaterContext = standaloneAppName ?? waterContext, so isFromUwms = activeWaterContext === 'uwms' is always true inside AquaRecycle. Feature code reads appStore.standaloneAppName === 'uwms' directly in a couple of places, e.g. isUwmsNativeApp in UwiPlantViewPage.jsx.
bootstrap.jsx vs app.jsx: bootstrap.jsx holds all startup wiring (MSAL, Google OAuth, theme, store provider, router). app.jsx is routes-only but still wraps a redundant ThemeProvider — AquaRecycle is "migrated" but not the cleanest (per standalone-apps.md).
// apps/uwms/src/app/app.jsx
export function App() {
const element = useRoutes(appRouter);
return <ThemeProvider theme={CustomTheme}>{element}</ThemeProvider>; // redundant ThemeProvider
}
Branding is title-only: apps/uwms/src/index.html sets <title>AquaRecycle</title>. Product colours/logo come at runtime from UwmsNavigationHelper (logo: assets.images.aquaRecycleLogo, brand Aqua/Recycle, teal nav palette) — not from a per-app theme file.
Navigation helper route table
UwmsNavigationHelper (libs/shared/src/helper/uwmsNavigationHelperInstance.js) is a singleton (.i getter). Its routes map drives the sidebar and Plant View buttons:
| Key | Path |
|---|---|
PLANT_VIEW / UWI | /uwi/plant-view |
ENERGY | /uwi/uwms-energy |
WATER_FLOW | /monitoring/source_category/SOURCE_CATEGORY |
WATER_STOCK | /monitoring/stock_category/STOCK_CATEGORY |
WATER_QUALITY | /monitoring/quality_category/QUALITY_CATEGORY |
AQUAGPT | /aquagpt |
ALERTS | /alerts/WATER_ALERTS |
REPORTS | /reports |
Visibility & Permissions
Routes by app
| Route | uwms (AquaRecycle) | production | Element |
|---|---|---|---|
/uwi → redirects to plant-view | ✅ routes.js | ✅ routes.js | UwiLayoutPage |
/uwi/plant-view | ✅ | ✅ | PlantViewPage |
/uwi/uwms-energy | ✅ | ✅ | UwmsEnergyPage |
Note:
uwmsdoes not register the standalone/energyorwater_balance/graphroutes — those areproduction-only. In AquaRecycle, STP energy lives entirely under/uwi/uwms-energy.
Permission tags & gating
| Tag | Gates | How |
|---|---|---|
UWI | Plant View & STP Energy sidebar entries | permissionId: 'UWI' on PLANT_VIEW and ENERGY in both nav helpers. In UwmsNavigationHelper their access is SidebarAccess.OPEN, so they are never locked in the standalone app; in the generic helper they use the default access and require the UWI permission. |
UWI_SHOW_ENERGY_SAVING | "Total Energy Saved" stat card and the Energy/CO₂ cards | PermissionWrapper in UWMSPlantViewStatsCard.jsx and UwiEnergyPage.jsx. |
UWI_DASHBOARD | Main-app dashboard variant (not the standalone plant view) | Toggle between standard and UWI dashboards via negate pattern in libs/dashboard/src/DashboardPage.jsx. |
DISABLE_ENERGY_TOGGLE_BUTTON | Energy toggle button (negative permission) | Hidden with negate when present; respected even for super users (see below). |
UWI_DASHBOARD super-user exception
isPermitted grants a SUPER_USER access to everything except DISABLE_* tags, ACCOUNT_SETTINGS, and — importantly — UWI_DASHBOARD:
// libs/shared/src/controller/permission/PermissionController.js
if (
(!ignoreSuperUser &&
allPermissions.includes('SUPER_USER') &&
!tag.startsWith('DISABLE_') &&
tag !== Permissions.allPermission.ACCOUNT_SETTINGS &&
tag !== Permissions.allPermission.UWI_DASHBOARD) ||
tag === Permissions.allPermission.WATER_MONITORING ||
tag === Permissions.allPermission.EFFICENCY
) {
return true;
}
So a super user must be explicitly granted UWI_DASHBOARD; the UWI and UWI_SHOW_ENERGY_SAVING route/feature tags follow the normal super-user override.
Negate pattern (dashboard toggle)
// libs/dashboard/src/DashboardPage.jsx
<PermissionWrapper tag={Permissions.allPermission.UWI_DASHBOARD} negate>
<BuildDashboardPage /> {/* standard dashboard */}
</PermissionWrapper>
<PermissionWrapper tag={Permissions.allPermission.UWI_DASHBOARD}>
<BuildUWIDashboardPage /> {/* UWI dashboard */}
</PermissionWrapper>
Sidebar lock state
Sidebar visibility resolves through getSidebarItemPermissionState (libs/components/src/permissionWrapper/SidebarPermissionCheck.jsx) using each item's access from SidebarAccess (OPEN, LOCKED, NEGATED, HIDDEN, STRICT, STRICT_HIDDEN). STRICT* forces ignoreSuperUser. Plant View's cross-module buttons filter themselves out when isLocked is true:
// libs/uwi/src/components/UwiPlantViewPage.jsx
NAV_BUTTONS.filter(({ navItemId }) => {
const navItem = UwmsNavigationHelper.i.sideBarDetails[navItemId];
const { isLocked } = getSidebarItemPermissionState(
{ tag: navItem?.permissionId, access: SidebarAccess.LOCKED },
appStore?.loginData
);
return !isLocked;
})
WATER_FLOW / WATER_STOCK / WATER_QUALITY sidebar items use SidebarAccess.STRICT with lockedPath values pointing at /feature_locked/... product pages.
Edge Cases
Loading states
- Plant stats:
isInitialLoad = uwmsStatsLoading && !uwmsStats; stat values render a spinner (StatValueLoader) only on first load, then show stale-while-refresh values (UWMSPlantViewStatsCard.jsx). - Energy graph: an overlay
CustomLoaderis shown over the graph whileuwiStore.loadingis true (UwiEnergyPage.jsx). - Filters: unit toggles are
disabledandpointerEvents: noneduring load (UnitWiseEnergyDetails.jsx).
Empty / no-data
- Missing sub-categories short-circuit the store effects (
if (!uwiDashboardData?.subCategories?.length) return;—UwiStore.js). - Stat values fall back to
'--'when null/undefined/NaN (formatStatValue,UWMSPlantViewStatsCard.jsx). getUwmsControlLogsnormalizes any of[],{logs},{data},{controlLogs}and returns[]otherwise (uwiController.js).
API errors / null handling
- Every data-source call is wrapped in
try/catchand returnsnullon failure rather than throwing (uwiDataSource.js). The store catches and setserror: 'Failed to fetch UWI data'while still clearingloadinginfinally(UwiStore.js). - Parsers defensively handle array-vs-object payloads and missing
meta(parseEnergyData,parseEnergySavedSum,uwiController.js). - Energy SI unit falls back to
'kWh'when the ENERGY_CATEGORY service orsiUnitis absent (uwiHelper.js,UwiEnergyPage.jsx).
Offline plant / device
- Device-level online/offline is surfaced through the embedded SCADA viewer and the monitoring pages the stat cards link to; the UWI layer itself renders
'--'for absent metrics rather than an offline badge.
Permission-denied
UWI_SHOW_ENERGY_SAVING-gated cards simply do not render for users lacking the tag (no error UI). Sidebar entries lackingUWIare locked/hidden per theirSidebarAccess.
Date-range boundaries
uwiEnum.Type(DAY|WEEK|MONTH|YEAR|CUSTOM) drives default dates viagetDefaultDateForType(start-of-year / start-of-month / today).- Week selection maps to a 7-day
CUSTOMwindow (getWeekDates); custom starts at month-start → today. - The month picker caps
maxDateto avoid future selection (UwiMonthPickerField.jsx). - "Since project began" reads
startDatefrom the cached login response and defaults to start-of-year if absent (uwiController.js), triggered by?sinceBegan=true/ theSINCE_PROJECT_BEGANmode (EnergyCarbonCards.jsx).
Auto-refresh gating
- The dashboard polls on
constants.refreshDuration, but only whenrange === CURRENT; historical/custom ranges do not auto-refresh (UwiStore.js). Header/plant stats refresh on their own interval (UwiStore.js).
Race conditions
- Energy-saved and CO₂ fetches guard against out-of-order responses with monotonically increasing request ids (
energySavedReqId/co2ReqId,UwiStore.js).
Remote-control / write states
- Register writes flow through the SCADA
RemoteControlPanel(ScadaController.writeRegisters) mounted viashowControlPanel; the panel manages its own confirm/pending/error UI, and actions are captured in the dated Remote Control Logs.
Dependencies
Shared store & services
AppStoreContext(libs/shared/src/store/AppStore.js) —standaloneAppName,setWaterContext,waterContext,loginData, derivedisFromUwms.apiClient(shared/services/api/apiClient) andUrls(shared/services/api/urls).AnalyticsService+AnalyticEvents—PAGE_VIEW,UWI_DASHBOARD_SESSION_TIME,UWI_TAB_SWITCH.LocalDBInstance/LocalDBKeys— reads cachedloginResponsefor project start date.Formatter,constants(refreshDuration,currentEnv,env).- uwiAI backend —
Urls.uwiAIBaseUrl()(Azure-hosted, env-switched).
Other libraries
components—PermissionWrapper,SidebarPermissionCheck/getSidebarItemPermissionState,If/IfNot,CustomLoader,MainLayout,SubPageWrapper,SearchComponent,graph/LineGraph, andpages/splash/splashController(createValidateLogin).scadaViewer—ScadaPageViewer(withshowControlPanel),RemoteControlPanel(writeRegisters),RemoteControlLogsDialog.monitoring— Flow / Stock / Quality pages the stat cards and buttons link into.featureLocked—FeatureLockedPage/FeatureLockedProductPagefor locked sidebar destinations.shared/helper—UwmsNavigationHelpersingleton.
External npm
moment, lodash, @mui/material + @mui/icons-material, @iconify/react, react-router-dom, and (via the shared graph components) recharts. App startup also uses @azure/msal-browser / @azure/msal-react, @react-oauth/google, and @fontsource/roboto.
Controllers / data sources
UwiDashboardController(libs/uwi/src/controller/uwiController.js)UwiDashboardDataSource(libs/uwi/src/dataSource/uwiDataSource.js)uwiEnum(libs/uwi/src/enums/uwiEnums.js),uwiHelper(libs/uwi/src/helper/uwiHelper.js)
Best Practices
1. Read derived context flags, not raw names
Prefer isFromUwms / activeWaterContext from the store over comparing standaloneAppName directly, so the same code works in the standalone app and in the main app's UWMS sub-context.
2. Always default nullable metrics
Follow the ?? '--' / formatStatValue pattern for any value that may be missing — backend calls return null on failure by design.
3. Gate energy savings explicitly
Wrap any energy-saved / CO₂ UI in PermissionWrapper tag={UWI_SHOW_ENERGY_SAVING}; do not assume super users see it via the override (they do for this tag, but the gate documents intent and supports non-super users).
4. Respect the refresh contract
Only the CURRENT range auto-refreshes. When adding new panels, hook them to the store's existing effects rather than starting independent intervals.
Troubleshooting
Sidebar shows Plant View but the page 404s in a new app
Cause: the app passed the wrong navInstance, so handleNavigationOnResponse looks up routes in the generic helper.
Solution: ensure <MainLayout navInstance={UwmsNavigationHelper} /> and standaloneAppName="uwms" are both set.
Energy Saved / CO₂ cards missing
Cause: user lacks UWI_SHOW_ENERGY_SAVING.
Solution: grant the permission; note super users get it via the override, but explicit grant is clearer.
UWI dashboard not showing for a super user
Cause: UWI_DASHBOARD is a deliberate super-user exception.
Solution: grant UWI_DASHBOARD explicitly (PermissionController.js).
Stats stuck on "--"
Cause: uwms/stats returned null (network/API error swallowed to null).
Solution: check the API/network tab; the UI intentionally degrades to '--' instead of erroring.
Graph does not update on date change
Cause: custom range requires both date1 and date2; the effect returns early if either is missing (UwiStore.js).
Solution: ensure the date picker sets both bounds for Week/Custom.
Analytics Integration
// libs/uwi/src/helper/useUwiSessionAnalytics.js
AnalyticsService.sendEvent(AnalyticEvents.PAGE_VIEW, {}, true); // on mount
AnalyticsService.sendEvent(AnalyticEvents.UWI_DASHBOARD_SESSION_TIME, { // on unmount
durationMs: duration,
});
Tab switches emit UWI_TAB_SWITCH with { tabIndex, tabName } — treatment_plant_status (Plant View) and efficiency_savings (STP Energy) — from UwiPlantViewPage.jsx and UwiEnergyPage.jsx.
Related Documentation
- Standalone Apps - The
uwms/ AquaRecycle standalone pattern - Permissions & Access Control -
UWI,UWI_DASHBOARD,UWI_SHOW_ENERGY_SAVING, super-user exceptions - Routes - Route registration across apps
- Energy Monitoring - Energy analytics used across the platform
Last Updated: July 2026
Module Location: libs/uwi/src/ (standalone app: apps/uwms/src/)