Skip to content

NMS-20100: PrimeVue Notifications page with a General configuration tab - #8709

Open
joseanesONMS wants to merge 9 commits into
jira/NMS-20100from
jira/NMS-20100-notifications-page
Open

NMS-20100: PrimeVue Notifications page with a General configuration tab#8709
joseanesONMS wants to merge 9 commits into
jira/NMS-20100from
jira/NMS-20100-notifications-page

Conversation

@joseanesONMS

@joseanesONMS joseanesONMS commented Jul 30, 2026

Copy link
Copy Markdown

First slice of the NMS-20100 epic (Notifications admin rework), targeting the epic feature branch jira/NMS-20100. Carries the PrimeVue Notifications page and a Configure Notifications dialog with only the General tab; the Event Notifications (NMS-20118), Destination Paths (NMS-20119) and Path Outages (NMS-20120) tabs follow in stacked PRs.

  • Notifications page: notice queries (per-user, outstanding, acknowledged), the outstanding-notices list with acknowledge, CSV export and printing, and collapsible explanations.
  • Configure Notifications dialog (gear icon) with the General tab: the system-wide notification on/off switch.
  • Adds admin-only /rest/notification-config endpoints for the notifd status plus the read side of destination paths (shared by the tab PRs), delegating to the same file-backed factories the legacy wizards use — the XML files stay the system of record.
  • Browser delivery of notices for /ui pages; the admin menu entry points at the new page.

Rewrites the Notifications page (notice queries, outstanding/acknowledged
notice lists with CSV export and printing, collapsible explanations) as a
PrimeVue page backed by REST, and adds a Configure Notifications dialog
holding a General tab with the system-wide notification on/off switch.

Adds the admin-only /rest/notification-config endpoints for the notifd
status and the read side of destination paths (the base API surface for the
dependent tab PRs), backed by the same file-backed factories the legacy
wizards use — the XML files stay the system of record. Browser delivery of
notices for /ui pages ships here as well. The Event Notifications,
Destination Paths and Path Outages tabs follow in dependent PRs.
@joseanesONMS

Copy link
Copy Markdown
Author

@marshallmassengill could you take a review pass when you get a chance? This is the base of the NMS-20100 stack — #8710, #8711 and #8712 each add one tab on top of it.

…uHeaderIT

The menu entry now lands on the Vue Notifications page, so the smoke test
waits for its page title instead of the legacy notification/index.jsp card
header. NotificationsPageIT still covers the legacy page, which remains
reachable by URL.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some stuff that I think is worth adjusting here:

Bugs worth fixing before merge

  1. The notices list can hang forever when /whoami fails. AdminNotifications.vue gates noticesStore.load() on authStore.loaded, via whenever(authLoaded, ...). But authStore.getWhoAmI() sets loaded.value = true only inside if (resp), and whoAmIService returns a falsy value on error. So if the whoami call fails for any reason, loaded stays false permanently, the whenever watcher never fires, and the user gets a page that renders "No outstanding notices found." with no spinner and no error — indistinguishable from genuinely having no notices. The legacy JSP had no such dependency. Setting loaded in a finally block (in authStore, which fixes the same latent issue for rolesAreLoaded) is the cleanest fix.

  2. One transient failure permanently bricks the config toggle. In ConfigureNotificationsDialog.vue, populated latches to true after the first populate() regardless of whether it succeeded. getNotificationConfigStatus() returns null on error, and the switch is :disabled="store.notifdStatus === null". A single failed GET — a server restart, a 500 — leaves the toggle disabled and the label reading "Notifications are unknown" for the lifetime of the page, and reopening the dialog won't retry because populated is already true. Either latch only on success, or re-populate on every open.

  3. Acknowledging the last row of the last page leaves you on a dead page. noticesStore.acknowledge() reloads without rewinding first. When the ack drops totalCount below the current offset, the reload returns zero rows and the component renders EmptyList even though notices exist. applyPreset resets first correctly; acknowledge should clamp it too.

Export path

  1. CSV and print silently truncate at 1000 notices. EXPORT_LIMIT = 1000 caps fetchAllForExport, and the print header then states ${rows.length} notices as though that were the whole set. A user exporting 5000 acknowledged notices gets 1000 with no indication anything was dropped. At minimum, compare against store.totalCount and surface a snackbar when the cap bites.

  2. CSV formula injection. escapeCell wraps in quotes and doubles embedded quotes, but doesn't neutralise a leading =, +, -, or @. Notice subject and text-message content derives from event data, which on a trap-fed system is externally influenced — so a crafted varbind can land a formula in the exported file that Excel will execute on open. Prefixing such cells with a single quote is the usual mitigation.

@marshallmassengill

Copy link
Copy Markdown
Contributor

So I think because of how you split this, we're going to need to do a final pass across everything once it's all in place. I do think this is a better approach though and makes things a bit easier to review.

A failed whoami no longer hangs loads gated on it, the config dialog
retries the status fetch on reopen, acknowledging the last row of the
last page rewinds a page, exports surface the 1000-notice cap, and CSV
cells are guarded against leading formula characters.
@joseanesONMS

Copy link
Copy Markdown
Author

All five addressed: the whoami fix went into authStore so it also covers rolesAreLoaded, the dialog now retries until the status loads, acking the last row rewinds a page, exports warn when the cap truncates (print header says "first 1000 of N"), and CSV cells get the quote-prefix guard. Agreed on a final pass across the stack once all four are in.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more changes here:

  1. Whoami fallback shows all users' notices under "Your Outstanding Notices." loaded now always flips, but whoAmI.id stays undefined, so effectiveUser → null and browseNotices drops the user filter. Make the yourOutstanding/userSearch presets refuse to run without a user id and surface an error instead of silently widening.
  2. Dead code in NotificationConfigRestService. Unused javax.ws.rs.DELETE and POST imports, the never-called isBlank, three stray blank lines before the closing brace, and a class javadoc claiming coverage of notifications.xml and notificationCommands.xml that this PR doesn't touch.
  3. Ack rewind steps back only one page. Clamp first to the last valid offset from totalCount rather than subtracting a single rows.
  4. sendStatusEvent sends only remoteUser; UpdateNotifdStatusServlet also sends remoteHost and remoteAddr. Match it or note the omission as deliberate.
  5. OnmsIconButton is driven by bare text/rounded fallthrough attrs instead of the seam's declared variant prop. Still two occurrences in NoticesTable.vue.
  6. IT hygiene. Shared target/test-work-dir/etc with opennms.home set globally and never restored; the "cold-start ordering regression guard" comment claims more than the test proves; testDestinationPathsReadable still carries a stale comment about sibling tests renaming paths.
  7. useBrowserNotifications has no test despite running on every /ui page — reconnect loop, array unwrapping, snackbar fallback all unverified.
  8. Rename AdminNotifications.vue → Notifications (the page isn't admin-gated; only the gear is).
  9. Declare all four tabs (final order + default) in ConfigureNotificationsDialog.vue with placeholder panels, and replace the populate() Promise.all with per-tab loaders. This removes most of the 7–8 file conflicts each sibling pair currently has, forces the tab-order decision to be deliberate rather than an artifact of merge sequence, and fixes #8710's retry-gap finding as a side effect.

Resolving these should get this into a state for merging at least.

The configuration dialog declares all four tabs with placeholder panels
and per-tab loaders that latch only on success: the sibling tab PRs each
replace one placeholder instead of restructuring the dialog, the tab
order is deliberate, and a failed load retries on the next visit.
User-scoped notice queries refuse to run without a user id instead of
silently widening to everyone's notices, and the ack rewind clamps to
the last valid page. The status event carries remoteHost and remoteAddr
like the legacy servlet, dead code is gone from the REST service, the
container is Notifications.vue since the page is not admin-gated,
OnmsIconButton uses its variant prop, the IT restores opennms.home, and
the browser-notification composable gets tests.
@joseanesONMS

Copy link
Copy Markdown
Author

All ten addressed. The dialog now declares all four tabs with placeholders and per-tab success-latching loaders, so the siblings each replace one panel — their conflicts collapsed to a loader entry and an import. User-scoped queries refuse to run without a user id, the ack clamp derives from totalCount, the status event carries remoteHost/remoteAddr, the container is Notifications.vue, the icon buttons use the variant prop, the IT restores opennms.home, and the composable has a test suite covering the reconnect loop, unwrapping and the snackbar fallback.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more but getting close I think... I'm honestly 50/50 on most of these because they are really minor and some of them are going to get addressed with the other PRs:

  • Dialog defaults to event-notifications, a placeholder here. Default to general until #8710 lands.
  • populate() and getDestinationPaths() now unreferenced, but notificationConfigStore.test.ts:70 still tests them.
  • sendStatusEvent reads request.getRemoteHost() outside the try — 500 after the status already persisted.
  • 127.0.0.1.tm0.epoch and tmlog0.log committed under opennms-webapp-rest/; all siblings inherit them.
  • gear button still text rounded
  • setRelativeHomeDirectory not restored

Open the config dialog on the live General tab instead of the Event
Notifications placeholder. Send the notifd status event entirely inside the
try so a reverse-DNS failure in getRemoteHost can't 500 a change that already
persisted. Drop the now-unreferenced destination-path plumbing from the config
store and its test. Remove two Atomikos JTA logs committed by mistake and
ignore that class of artifact. Clarify that restoring opennms.home fully undoes
the IT's setRelativeHomeDirectory calls, and tidy the icon-button variant and a
trailing gap.
@marshallmassengill
marshallmassengill self-requested a review August 3, 2026 12:28

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple more changes needed:

The menu entry has roles: null, so every user lands on this page, and Acknowledge issues PUT /rest/notifications/{id}, which falls through to the /rest/** catch-all restricted to ROLE_REST,ROLE_ADMIN. Legacy /notification/acknowledge was under /** → ROLE_USER. The fix has precedent ten lines up in the same security file: the alarm block carves out /rest/alarms/*?ack=true for ROLE_USER with !hasRole('ROLE_READONLY').

useRole.ts has no ROLE_READONLY at all, so the button renders for read-only users where list.jsp explicitly hid it

There is also a regression here: fetchAllForExport rebuilds the preset→user mapping itself instead of going through the guarded load(), so it still fails open on a failed whoami.

@joseanesONMS
joseanesONMS requested a review from synqotik August 3, 2026 13:59
Acknowledging a notification now has its own /rest/notifications/** security
block granting ROLE_USER (not ROLE_READONLY), mirroring the alarms rules, so a
normal user can ack as they could under the legacy servlet while notice config
editing stays REST/ADMIN-only under /rest/notification-config. The UI adds
ROLE_READONLY/ROLE_MOBILE and hides the Acknowledge action from read-only users,
and the CSV/print export now routes through the store's whoami guard so a failed
whoami can no longer widen a user-scoped export to everyone's notices.

NotificationManager and DestinationPathManager route every mutation through a
saveWithRollback helper that snapshots the in-memory model, applies the change
and saveCurrent(), and restores the snapshot on any failure. This stops a change
that leaves the config unmarshallable — removing the last notification/path, or a
field-by-field replace that throws mid-way — from leaving memory diverged from
the on-disk file until a restart.

@marshallmassengill marshallmassengill left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth-pass review. The saveWithRollback work is the right fix and it resolves the in-memory/on-disk divergence that was blocking #8710 (varbind) and #8711 (deleting the last destination path) by inheritance, so both of those downgrade once the siblings rebase. aFailedReplaceLeavesNoHalfUpdatedEntry covers the hardest case. The ack authz fix, the read-only hide and the export whoami guard all look right, and canAcknowledgeNotifications fails closed on a failed whoami.

One blocker inline: the new PUT rule is broader than its stated intent and lets a normal user mass-unacknowledge everything. The rest are smaller.

Two follow-ups that have no line to attach to here:

  1. A shared serverDetail(err, fallback) helper in ui/src/services/serviceHelpers.ts. Several server-side guards carry a message that is the only explanation the user gets, and the sibling PRs' delete and status calls discard err.response.data. I'll raise it on #8710 and #8711 where it's consumed.
  2. addPath and addNotification rollback cases in the two new test classes. Both suites cover removal, and NotificationManager also covers replace, but neither covers an add.

Also worth knowing: all three siblings are still based on e0bcb101c65, one commit behind this.

Comment thread opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml Outdated
Comment thread ui/src/services/notificationConfigService.ts Outdated
Comment thread ui/src/stores/notificationConfigStore.ts Outdated
Comment thread ui/src/components/AdminNotifications/ConfigureNotificationsDialog.vue Outdated
Comment thread opennms-config/src/main/java/org/opennms/netmgt/config/NotificationManager.java Outdated
Comment thread ui/tests/stores/notificationConfigStore.test.ts
@marshallmassengill
marshallmassengill self-requested a review August 3, 2026 16:36
…oaders

Splits the notification PUT security rule: only the single-resource path
(/rest/notifications/*) is granted to ROLE_USER, while the collection PUT — which
acks or unacks by arbitrary criteria and would let a user mass-unacknowledge the
whole system — stays REST/ADMIN. The save-rollback in both config managers now
restores inside its own try/catch so a failing restore adds itself as a suppressed
exception rather than replacing the original cause, and the shallow-snapshot
constraint in DestinationPathManager is documented. getDestinationPaths returns
null on failure and getStatus returns a boolean, so the general tab loader reports
real success instead of inferring it; a store test pins that contract.
@joseanesONMS

Copy link
Copy Markdown
Author

All addressed in the latest push:

  • Ack authz scope: split the notification PUT rule — only the single-resource path (/rest/notifications/*) is granted to ROLE_USER; the collection PUT (acks/unacks by arbitrary criteria, so a mass-unack vector) stays REST/ADMIN, with both rules present so no PUT falls through unauthorized.
  • Rollback cause preservation: both config managers now restore inside their own try/catch and addSuppressed a failed restore rather than replacing the original cause; the shallow-snapshot constraint in DestinationPathManager is documented.
  • Loader contract: getDestinationPaths returns null on failure and getStatus returns a boolean, so the general tab loader reports real success (() => store.getStatus()); added a store test pinning it.

Noted your point that the authz rule can't be covered by a module IT (the filter chain isn't installed in AbstractSpringJerseyRestTestCase) — verifying via smoke / a two-account manual check.

@marshallmassengill

Copy link
Copy Markdown
Contributor

With that last one, if it goes green then I think this is ready to merge pending approval from @synqotik. This is going to require the other PRs go in after so it may be that we want to get them all lined up at once or we may want to get this one merged and then rebase the others. Apologies for the churn but this is in a much better state now.

@marshallmassengill
marshallmassengill self-requested a review August 3, 2026 17:06
…ack gap

/rest/notifications/* also matches the trailing-slash form /rest/notifications/,
which JAX-RS routes to the criteria-based collection ack/unack (the {notifId}
regex can't match an empty segment), leaving the mass-unack reachable by a
ROLE_USER. Pin the trailing-slash form to REST/ADMIN ahead of the single-resource
rule; the single-resource and collection rules are unchanged.
@joseanesONMS

Copy link
Copy Markdown
Author

Good catch on the trailing slash — fixed in 5bf81b2. Pinned /rest/notifications/ (trailing-slash form) to REST/ADMIN ahead of the narrow /rest/notifications/* rule, so the criteria-based collection ack/unack is no longer reachable by ROLE_USER; the single-resource and /** collection rules are unchanged. Order is trailing-slash → single-resource → collection, first match wins.

Agreed this one isn't module-IT-coverable (the filter chain isn't installed in AbstractSpringJerseyRestTestCase) — will confirm with a two-account curl -X PUT against the deployed rules.

Migrate the notifications page chrome — the Configure dialog (dialog +
tabs + toggle), the notices table, and the queries bar — onto the
Onms-* seam wrappers so the page passes the no-direct-primevue lint
rule. Behaviour and markup are unchanged; iftalabel stays on PrimeVue
as it has no wrapper yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants