Skip to main content

Alerts & Notifications

A full-page view of monitoring alerts — list, filter, sort, search, resolve, assign, and export. Alerts split into two tabs: General Alerts and Device Status Alerts (offline).

At a glance

Route/alerts/:categoryIdAlertsPage (libs/alerts/src/AlertsPage.jsx, exported as AlertsPageV2)
PermissionALERTS gates the sidebar entry; the route itself is not wrapped
Librarylibs/alerts/ is UI only — all state and API work lives in libs/shared (AlertsStore, alertsController, alertsDataSource, AlertsHelper)
Two domains:categoryId = WATER_ALERTS or ENERGY_ALERTS — see Energy alerts
List endpointGET alerts
PollGET alerts with type: daily, every 5 min, for the tab badges
ResolveNo resolve endpoint — assign-to-self + mark-read, behind a 3-second undo
ExportAn xlsx report URL opened in a new tab
FilteringEntirely client-side over the fetched month — no pagination
What Alerts does NOT do
  • There is no resolve endpoint. "Resolved" is assign-to-self plus mark-read, and nothing is sent until the 3-second undo window closes.
  • It does not paginate. One month is fetched and every filter, sort, and search runs in the browser over that payload.
  • It does not page-fetch on filter changes — only the tab and the month refetch.
  • libs/alerts holds no state. The store, controller, and data source all live in libs/shared.
  • Energy mode is not a different endpoint or UI — one flag swaps the dataset, and the poll and export do not honour it.

Layout

Summary: The left rail is sticky at 272 px on md+; everything it and the header offer is applied to an already-fetched month in the browser. Only the tab and the month actually refetch.

AreaComponentWhat it offersOn change
TabsAlertsToggleTypeHeaderGeneral / Device Status, each with a red (+N) today-unread badge; disabled while loadingRefetches — clears the list, swaps alert_type, rewrites the query string
Searchsame"Search alerts here", debounced 1 sClient-side, matches the alert's heading
Sort menuAlertsSortListSort By Date · Sort By Unresolved · a Show Read itemClient-side reorder / reveal
Month + exportAlertsLeftComponentAppDatePickerSelection in month mode + a download iconMonth refetches; download opens a report URL
Stats piesamePieChartHover over unread / read / resolved, with the month total in the centre
Unit filterAlertFilterComponentA dialog of unit chips taken from your stored login dataClient-side filter
ListAlertListComponentA Today group and a monthly groupRow opens monitoring and marks read
Row actionsexpanded rowResolve, AssignSee below

How the list is built

Every control except the tab and the month is a client-side pass over the fetched data:

Summary: Four independent drop filters, then a sort, then the search. Because it all runs on one month's payload, a wide search still only reaches that month.

The monthly group carries a red total − resolved Unresolved Alerts badge, hidden whenever a unit filter is active. The Today group only appears for the current month, and shows "Congratulations! You have no alerts." when empty.


Resolving and assigning

Resolve is optimistic, with a real undo window:

Summary: Nothing is sent until the snackbar closes. Undo simply drops it — there is no compensating call, because no call was made yet.

There is no "resolve" endpoint

"Resolved" is expressed as assignee = you plus read. The two PATCHes go to /notification/assignee and notification/updateRead; nothing carries a resolve flag. Assigning to another user is the same /notification/assignee path via POST.


Click-through routing

Rows route by standardCategoryId, always carrying the alert's own date (converted from DD-MM-YYYY to DD/MM/YYYY) and opening in a new tab:

CategoryTarget
SOURCE_CATEGORY/monitoring/source_category/{categoryId}/{subCategoryId}/{unitId}
STOCK_CATEGORY/monitoring/stock_category/{categoryId}/?unitId={unitId}&showGraph=true
ENERGY_CATEGORYThe energy monitoring route, ?unitId={unitId}
anything else/monitoring/{standardCategoryId lowercased}/{categoryId}/?unitId={unitId}

Navigation happens first, then the alert is marked read, then ALERTS_CLICK fires.


Data & APIs

ActionMethodPathTrigger
List alertsGETalertsMount, tab change, month change, and after every read/assign
Today's unreadGETalertsMount + every 5 min
Mark readPATCHnotification/updateReadRow click, and as part of resolve
Assign to a userPOST/notification/assigneeThe row's assign action
Assign to selfPATCH/notification/assigneeThe resolve flow
By date rangeGETalerts/dateRangeUsed elsewhere, not by this page
ExportGET{baseUrl}report?... via window.openThe download icon
Params, response shape, and the alert object

List params

ParamValue
dateDD/MM/YYYY — any day in the month you want
alert_typegeneral or offline
energyEnabledtrue when the route param is ENERGY_ALERTS

Poll params{ date, type: 'daily' } only.

Response, keyed by alert type:

{
"generalAlerts": {
"alerts": { "today": [ /* ... */ ] }, // plus the month's buckets
"meta": { "total": 0, "resolved": 0, "unread": 0, "read": 0 },
"pieChartData": [ /* injected client-side by the controller */ ]
},
"offlineAlerts": { /* same shape */ },
"todaysUnread": { "generalAlerts": 0, "offlineAlerts": 0 }
}

The controller adds pieChartData by walking unread / read / resolved out of meta and attaching a colour and a label element to each.

Fields an alert row uses

FieldUsed for
idMark-read, assign, and ?alertId deep-link matching
date (DD-MM-YYYY) · date_keyNavigation date · sorting and the mark-read payload
typeMark-read payload and analytics
standardCategoryId · categoryId · subCategoryId · unitIdClick routing and the unit filter
isRead · resolvedThe Show Read / Show Resolved filters
importanceLevelRow colour — high and medium are mapped; nothing else is
body · subTitleRow text; body === 'online' also drives the Show Online filter
description.headingThe only field search looks at
meta.paramAppended for quality alerts

Export URLreport?useShift=true&reportType=granular&reportFormat=xlsx&service=alert&jwt=<token>&startDate=<month start>&endDate=<month end>, opened with window.open.

Analytics

EventPayload
PAGE_VIEW
ALERTS_CLICKalertType, categoryId, importanceLevel, clickedTime
ALERTS_DOWNLOADThe whole export config
ALERT_ASSIGNEDOn assignment

Alert types

TabidQuery keyResponse key
General AlertsGENERAL_ALERTSgeneralgeneralAlerts
Device Status AlertsOFFLINE_ALERTSofflineofflineAlerts

AlertsHelper also defines a general_offline query key and a GENERAL_OFFLINE_ALERTS list type that no tab uses, plus a commented-out Insights / Recommendations type.


Energy alerts

The :categoryId route segment picks the domain and sets exactly one store flag — energyEnabled = (categoryId === 'ENERGY_ALERTS').

categoryIdReached fromLoads
WATER_ALERTSThe main and standalone app sidebarsWater alerts
ENERGY_ALERTSThe energy monitoring navEnergy alerts

It is the same GET alerts endpoint; energyEnabled just rides along in the params. Layout, tabs, filters, and the pie are identical — only the dataset changes, and energy rows deep-link into energy monitoring.

Energy scope is only half wired

Only the list call passes energyEnabled. The 5-minute poll ({ date, type: 'daily' }) and the export (service=alert) do not — so the tab unread badges and the downloaded report are not energy-scoped even in energy mode.

Separately, the tab switcher re-applies energyEnabled as a query parameter, but the store only ever reads it from the route param. The query string is decorative; the route segment is the real source.


Render states

StateWhat the user sees
LoadingA loader in the list; the tabs are disabled
No data returnedThe whole tab area falls back to PageNotFound
No alerts today"Congratulations! You have no alerts." in the Today group
Non-current monthThe Today group is not rendered at all
Deep link?alertId=<id> expands and scrolls to that alert — and starts the page with Show Read on so a read alert is still visible
?alertType=Preselects the tab on load
API failure⚠️ The spinner hangs forever — see below

Visibility & access

Which apps register the route

/alerts/:categoryId is registered wherever the alerts nav item exists. The :categoryId segment — WATER_ALERTS or ENERGY_ALERTS — selects the domain, and energy is reached through the energy navigation helper rather than the main sidebar.

Route-level gating: none

The route element is unwrapped. ALERTS gates the sidebar entry only, so the URL renders for anyone who reaches it.

Where the store actually lives

libs/alerts is UI only. AlertsStoreContextProvider sits in libs/shared, and it guards its own fetch with window.location.pathname.includes('/alerts') — because the provider is mounted more widely than this page. The 5-minute today-poll, however, runs unconditionally wherever the provider is mounted.

Query parameters that change behaviour

ParameterEffect
?alertType=general|offlinePreselects the tab on load
?alertId=<id>Expands and scrolls to that alert, and starts the page with Show Read enabled
?energyEnabled=trueRe-applied by the tab switcher, but cosmetic — the store reads the route segment, never this

Dependencies

Shared store & services

ImportUsed for
AlertsStoreAll state: params, init, poll, resolve/assign, export
AlertsController · AlertsDataSourceThe five alert calls
AlertsHelperAlert types, importance colours, pie colours, sort options
AppStoreloginData.token for the export URL, loginData.userId for assign-to-self
apiClient · UrlsTransport
AnalyticsService · AnalyticEventsPAGE_VIEW, ALERTS_CLICK, ALERTS_DOWNLOAD, ALERT_ASSIGNED
DateFormatterMonth start/end for the export range
ReportType · ReportFormatThe export is a granular / xlsx report
NavigationHelper · EnergyNavigationHelper · useNavigateSearchParamsRow click-through
constantsrefreshDuration

Component library

SubPageWrapper, FixedBar, If, PageNotFound, GenericInfo, PieChartHover, AppDatePickerSelection, plus MUI Tabs, Snackbar, Dialog.

Lib-internal

AlertsPage (exported as AlertsPageV2), AlertTabView, AlertsLeftComponent, AlertListComponent, AlertFilterComponent, AlertsSortList.

External npm

moment, lodash (1-second search debounce plus the filter chain), @mui/material, @iconify/react. The pie is Recharts-based via the shared component; this lib imports no chart library directly.


Usage examples

Read the current tab's alerts and counts

import { useContext } from 'react';
import { AlertsStoreContext } from '@aquagen-mf-webapp/shared/store/AlertsStore';
import { AlertsHelper } from '@aquagen-mf-webapp/shared/helper/alertHelperInstance';

function UnreadBadge({ alertTypeId }) {
const { notificationData, loading } = useContext(AlertsStoreContext);
if (loading) return null;

const key = AlertsHelper.i.alertTypes[alertTypeId].responseApiKey; // 'generalAlerts'
const unread = notificationData?.todaysUnread?.[key];
return unread ? <span>(+{unread})</span> : null;
}

Switch tab or month

const { params, setParams, setNotificationData } = useContext(AlertsStoreContext);

setNotificationData(null); // clear first, as the tab switcher does
setParams({
...params,
alert_type: AlertsHelper.i.alertTypes.OFFLINE_ALERTS.queryApiKey, // 'offline'
});

setParams({ ...params, date: '01/07/2026' }); // any day in the target month

Mark read, assign, and export

const {
updateNotificationRead,
assignNotification,
updateAssignNotification,
downloadAlerts,
} = useContext(AlertsStoreContext);

await updateNotificationRead({ id: alert.id, date: alert.date_key, type: alert.type });
await updateAssignNotification({ notificationId: alert.id, assigneeId: myUserId }); // resolve
downloadAlerts(); // opens the xlsx report URL in a new tab

Fire the alerts analytics events

AnalyticsService.sendEvent(AnalyticEvents.PAGE_VIEW, {}, true);
AnalyticsService.sendEvent(AnalyticEvents.ALERTS_CLICK, {
alertType: alert.type,
categoryId: alert.categoryId,
importanceLevel: alert.importanceLevel,
clickedTime: moment().format('DD/MM/YYYY HH:mm:ss'),
});

Troubleshooting

The spinner never stops

init() has no try/catch, so a rejected request skips setLoading(false) permanently. Only a reload recovers. This is the single most common failure on this page.

Turning on "Show Read" also revealed resolved alerts

Known defect — the one menu item toggles both flags. After a ?alertId deep link the two start out of step, so the first click can show resolved while hiding read.

Searching finds nothing that is clearly on screen

Search matches only description.heading. Text in the row body is not indexed.

An alert I resolved is back

Resolve is deferred for 3 seconds behind the Undo snackbar. If the snackbar was dismissed via Undo, nothing was ever sent.

The energy tab's badges look like water numbers

The 5-minute poll does not pass energyEnabled, so the badge counts are not energy-scoped. The exported report has the same gap.

The exported file covers units I filtered out

The export config builds a unitId that is never appended to the URL, so exports always span every unit.

The page renders "not found"

AlertTabView falls back to PageNotFound once loading finishes with no notificationData — including after a swallowed error.


Known issues & gotchas

Verified against the code
IssueWhereImpact
Perpetual loader on any API failureinit() in the shared AlertsStore sets loading true, awaits, then sets it false — with no try/catch. The data source also reads response.data unguardedOne failed request leaves the page spinning forever; only a reload recovers
"Show Read" also flips "Show Resolved"The single menu item toggles both flags, each from its own current valueThey can drift apart — a ?alertId deep link starts showRead: true, showResolved: false, so the first click shows resolved while hiding read
"Show Online" has no controlshowOnline is initialised true, is used in the filter, but no UI ever changes itThe body === 'online' filter is unreachable dead logic
Unguarded response keysThe controller reads data?.[key].meta and response?.[key].alerts?.today — the optional chain stops before the keyA response missing generalAlerts / offlineAlerts throws instead of degrading
Unguarded type lookupThe controller does _.filter(...)[0].id on the alert typesAn unexpected alert_type throws
Unguarded session parseThe unit filter builds its chip list from JSON.parse(...) of the stored login responseThrows if the session is missing
JWT in the export URLThe download URL carries jwt=<token> in the query string, opened via window.openThe token lands in browser history and any intermediary logs
Dead export fieldThe export config builds a unitId: '' that is never appended to the URLExports always cover every unit, regardless of the active filter
Search is single-fieldOnly description.heading is matchedText visible in the row body is not searchable
Leftover debug logconsole.log('alert') at the top of the row-click handlerConsole noise on every click

Code reference

FileRole
AlertsPage.jsxShell, tabs + badges, search, sort header
components/AlertTabView.jsxTwo-column layout, PageNotFound guard, resolve snackbar
components/AlertsLeftComponent.jsxMonth picker, export, stats pie, filter mount
components/AlertListComponent.jsxGrouping, the filter/sort pipeline, click routing
components/AlertFilterComponent.jsxUnit-chip filter dialog
components/AlertsSortList.jsxSort menu + the Show Read item
libs/shared/src/store/AlertsStore.jsParams, init, poll, resolve/assign actions, export
libs/shared/src/controller/alertsController.jsPie-data shaping, today-alert merging
libs/shared/src/dataSource/alertsDataSource.jsThe raw calls
libs/shared/src/helper/alertHelperInstance.jsAlert-type, colour, and sort enums