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/:categoryId → AlertsPage (libs/alerts/src/AlertsPage.jsx, exported as AlertsPageV2) |
| Permission | ALERTS gates the sidebar entry; the route itself is not wrapped |
| Library | libs/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 endpoint | GET alerts |
| Poll | GET alerts with type: daily, every 5 min, for the tab badges |
| Resolve | No resolve endpoint — assign-to-self + mark-read, behind a 3-second undo |
| Export | An xlsx report URL opened in a new tab |
| Filtering | Entirely client-side over the fetched month — no pagination |
- 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/alertsholds no state. The store, controller, and data source all live inlibs/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.
| Area | Component | What it offers | On change |
|---|---|---|---|
| Tabs | AlertsToggleTypeHeader | General / Device Status, each with a red (+N) today-unread badge; disabled while loading | Refetches — clears the list, swaps alert_type, rewrites the query string |
| Search | same | "Search alerts here", debounced 1 s | Client-side, matches the alert's heading |
| Sort menu | AlertsSortList | Sort By Date · Sort By Unresolved · a Show Read item | Client-side reorder / reveal |
| Month + export | AlertsLeftComponent | AppDatePickerSelection in month mode + a download icon | Month refetches; download opens a report URL |
| Stats pie | same | PieChartHover over unread / read / resolved, with the month total in the centre | — |
| Unit filter | AlertFilterComponent | A dialog of unit chips taken from your stored login data | Client-side filter |
| List | AlertListComponent | A Today group and a monthly group | Row opens monitoring and marks read |
| Row actions | expanded row | Resolve, Assign | See 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.
"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:
| Category | Target |
|---|---|
SOURCE_CATEGORY | /monitoring/source_category/{categoryId}/{subCategoryId}/{unitId} |
STOCK_CATEGORY | /monitoring/stock_category/{categoryId}/?unitId={unitId}&showGraph=true |
ENERGY_CATEGORY | The 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
| Action | Method | Path | Trigger |
|---|---|---|---|
| List alerts | GET | alerts | Mount, tab change, month change, and after every read/assign |
| Today's unread | GET | alerts | Mount + every 5 min |
| Mark read | PATCH | notification/updateRead | Row click, and as part of resolve |
| Assign to a user | POST | /notification/assignee | The row's assign action |
| Assign to self | PATCH | /notification/assignee | The resolve flow |
| By date range | GET | alerts/dateRange | Used elsewhere, not by this page |
| Export | GET | {baseUrl}report?... via window.open | The download icon |
Params, response shape, and the alert object
List params
| Param | Value |
|---|---|
date | DD/MM/YYYY — any day in the month you want |
alert_type | general or offline |
energyEnabled | true 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
| Field | Used for |
|---|---|
id | Mark-read, assign, and ?alertId deep-link matching |
date (DD-MM-YYYY) · date_key | Navigation date · sorting and the mark-read payload |
type | Mark-read payload and analytics |
standardCategoryId · categoryId · subCategoryId · unitId | Click routing and the unit filter |
isRead · resolved | The Show Read / Show Resolved filters |
importanceLevel | Row colour — high and medium are mapped; nothing else is |
body · subTitle | Row text; body === 'online' also drives the Show Online filter |
description.heading | The only field search looks at |
meta.param | Appended for quality alerts |
Export URL — report?useShift=true&reportType=granular&reportFormat=xlsx&service=alert&jwt=<token>&startDate=<month start>&endDate=<month end>, opened with window.open.
Analytics
| Event | Payload |
|---|---|
PAGE_VIEW | — |
ALERTS_CLICK | alertType, categoryId, importanceLevel, clickedTime |
ALERTS_DOWNLOAD | The whole export config |
ALERT_ASSIGNED | On assignment |
Alert types
| Tab | id | Query key | Response key |
|---|---|---|---|
| General Alerts | GENERAL_ALERTS | general | generalAlerts |
| Device Status Alerts | OFFLINE_ALERTS | offline | offlineAlerts |
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').
categoryId | Reached from | Loads |
|---|---|---|
WATER_ALERTS | The main and standalone app sidebars | Water alerts |
ENERGY_ALERTS | The energy monitoring nav | Energy 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.
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
| State | What the user sees |
|---|---|
| Loading | A loader in the list; the tabs are disabled |
| No data returned | The whole tab area falls back to PageNotFound |
| No alerts today | "Congratulations! You have no alerts." in the Today group |
| Non-current month | The 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
| Parameter | Effect |
|---|---|
?alertType=general|offline | Preselects the tab on load |
?alertId=<id> | Expands and scrolls to that alert, and starts the page with Show Read enabled |
?energyEnabled=true | Re-applied by the tab switcher, but cosmetic — the store reads the route segment, never this |
Dependencies
Shared store & services
| Import | Used for |
|---|---|
AlertsStore | All state: params, init, poll, resolve/assign, export |
AlertsController · AlertsDataSource | The five alert calls |
AlertsHelper | Alert types, importance colours, pie colours, sort options |
AppStore | loginData.token for the export URL, loginData.userId for assign-to-self |
apiClient · Urls | Transport |
AnalyticsService · AnalyticEvents | PAGE_VIEW, ALERTS_CLICK, ALERTS_DOWNLOAD, ALERT_ASSIGNED |
DateFormatter | Month start/end for the export range |
ReportType · ReportFormat | The export is a granular / xlsx report |
NavigationHelper · EnergyNavigationHelper · useNavigateSearchParams | Row click-through |
constants | refreshDuration |
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
| Issue | Where | Impact |
|---|---|---|
| Perpetual loader on any API failure | init() in the shared AlertsStore sets loading true, awaits, then sets it false — with no try/catch. The data source also reads response.data unguarded | One 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 value | They 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 control | showOnline is initialised true, is used in the filter, but no UI ever changes it | The body === 'online' filter is unreachable dead logic |
| Unguarded response keys | The controller reads data?.[key].meta and response?.[key].alerts?.today — the optional chain stops before the key | A response missing generalAlerts / offlineAlerts throws instead of degrading |
| Unguarded type lookup | The controller does _.filter(...)[0].id on the alert types | An unexpected alert_type throws |
| Unguarded session parse | The unit filter builds its chip list from JSON.parse(...) of the stored login response | Throws if the session is missing |
| JWT in the export URL | The download URL carries jwt=<token> in the query string, opened via window.open | The token lands in browser history and any intermediary logs |
| Dead export field | The export config builds a unitId: '' that is never appended to the URL | Exports always cover every unit, regardless of the active filter |
| Search is single-field | Only description.heading is matched | Text visible in the row body is not searchable |
| Leftover debug log | console.log('alert') at the top of the row-click handler | Console noise on every click |
Code reference
| File | Role |
|---|---|
AlertsPage.jsx | Shell, tabs + badges, search, sort header |
components/AlertTabView.jsx | Two-column layout, PageNotFound guard, resolve snackbar |
components/AlertsLeftComponent.jsx | Month picker, export, stats pie, filter mount |
components/AlertListComponent.jsx | Grouping, the filter/sort pipeline, click routing |
components/AlertFilterComponent.jsx | Unit-chip filter dialog |
components/AlertsSortList.jsx | Sort menu + the Show Read item |
libs/shared/src/store/AlertsStore.js | Params, init, poll, resolve/assign actions, export |
libs/shared/src/controller/alertsController.js | Pie-data shaping, today-alert merging |
libs/shared/src/dataSource/alertsDataSource.js | The raw calls |
libs/shared/src/helper/alertHelperInstance.js | Alert-type, colour, and sort enums |
Related
- Dashboard — the right-rail alert list reuses this store's mark-read call
- Water Monitoring · Energy Monitoring — where a row click lands
- Reports — the same report service that backs the export
- Application Routes · Permissions