Skip to content

NMS-20108: On-Call Roles management via versioned REST API and PrimeVue UI - #8715

Open
joseanesONMS wants to merge 15 commits into
developfrom
jira/NMS-20108-oncall-roles-pr
Open

NMS-20108: On-Call Roles management via versioned REST API and PrimeVue UI#8715
joseanesONMS wants to merge 15 commits into
developfrom
jira/NMS-20108-oncall-roles-pr

Conversation

@joseanesONMS

Copy link
Copy Markdown
Contributor

Rewrites Admin → On-Call Roles as a PrimeVue page backed by a new versioned REST API, keeping groups.xml as the system of record.

  • Adds admin-only /api/v2/on-call-roles: list/get/create/update/rename/delete plus a computed month calendar that reuses the exact interval resolution notifd uses; time-zone is reported so clients render times in server time.

  • Hand-edited roles round-trip untouched and stay editable: file changes are picked up on read, schedule entries already stored skip new-entry validation, the optional time id attribute is preserved, and a role whose schedule the runtime cannot evaluate reports schedule-error instead of failing the list.

  • New entries are validated and canonicalized to the exact forms BasicScheduleUtils dispatches on (zero-padded widths, English month names, lowercase weekday, unpadded monthly day); overnight begins>ends entries are rejected because the runtime never matches them.

  • The page shows the legacy list columns plus an explicit supervisor-fallback indicator, a month calendar with one-off coverage add/remove (content-matched removal, refetch-before-mutate, confirmation before removing recurring entries), rename/delete with referential warnings, and inline field validation with API errors shown in the dialogs.

  • The admin menu entry now points at the new page. 16 integration tests cover the API; store, validation and dialog behavior are covered by vitest.

  • Replaces fork PR NMS-20108: On-Call Roles management via versioned REST API and PrimeVue UI #8702 with an in-repo branch so CI runs.

Adds an admin-only versioned /api/v2/on-call-roles API over the existing
groups.xml role storage (list/get/create/update/rename/delete plus a computed
month calendar from GroupManager.getRoleScheduleEntries), and a PrimeVue
Manage On-Call Roles page with a schedule calendar and one-off coverage
editor. The admin menu entry now points at the new page.

Roles stay in groups.xml: hand-edited entries (including recurring weekly,
daily and monthly schedules) are picked up on read via GroupManager.update(),
round-trip unmodified, and remain editable; schedule validation applies only
to entries added through the API. New overnight duty entries (begin after
end) are rejected because DutySchedule never matches them at runtime.
Review fixes for the on-call roles API and page. New schedule entries are
canonicalized to the exact forms the scheduler dispatches on (zero-padded
widths, JVM-default-locale month names, lowercase weekly days, unpadded
monthly days), closing the gap where the API accepted entries notifd
silently ignored. The optional time id attribute now round-trips, partial
updates can change supervisor or membership-group independently, and a role
whose schedule cannot be evaluated no longer breaks the whole list response.

The calendar reports the server time zone and the dialog renders and labels
all times in it, so what is entered matches what notifd evaluates. Coverage
edits re-read the stored role before mutating and match removals by content,
narrowing the window where a concurrent edit could be overwritten; removing
a recurring (hand-edited) entry asks for confirmation since this editor
cannot recreate one. Dialogs show API rejections inline like the users and
groups pages.
…rver locale

Formatting with the default locale made specific-schedule writes fail only
in months whose abbreviation breaks the 20-character width (janv. but not
mai), and stored strings the API itself could not re-parse. Dates are now
always stored in the fixed-width Locale.ROOT form; if the default locale
cannot parse that form back, the write is rejected up front with a clear
message, since notifd would silently ignore the entry at runtime.
A role whose stored schedule the runtime cannot evaluate now reports a
schedule-error field instead of silently showing an empty currently-on-call
list, and the roles table renders it as a warning tag — an unevaluable rota
is a notifd outage, not an idle one. The name regex scans for forbidden
characters instead of a newline-bypassable full-string match.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill could you take a review pass when you get a chance?

@joseanesONMS
joseanesONMS requested a review from synqotik July 30, 2026 20:52
… in MenuHeaderIT

The menu entry now lands on the Vue page, so the smoke test waits for its
page title instead of the legacy JSP breadcrumb.
@joseanesONMS
joseanesONMS force-pushed the jira/NMS-20108-oncall-roles-pr branch from bc553c8 to 8e55125 Compare July 30, 2026 23:37
Swap direct PrimeVue for the Onms-XXX seam wrappers across the roles
table, editor/rename dialogs and the schedule calendar: Button->OnmsButton
(text/outlined mapped to variant), Dialog->OnmsDialog,
InputText->OnmsInputText, Select->OnmsSelect, DatePicker->OnmsDatePicker,
DataTable->OnmsTable, Column->OnmsColumn, Tag->OnmsTag. Message and
IftaLabel have no wrapper yet and stay on PrimeVue. No behaviour change.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@synqotik @marshallmassengill — ready for review. This page now uses the @opennms/onms-ui Onms-XXX wrappers instead of direct PrimeVue (Button, Dialog, InputText, Select, DatePicker, DataTable, Column, Tag). Message and IftaLabel have no wrapper yet, so they stay on PrimeVue. pnpm lint is clean (ran lint:fix), and the vue-tsc build and the UI test suite pass. No behaviour change.

…sDialog width prop

Month nav and remove-coverage buttons now use OnmsIconButton with icon
components instead of a fallthrough icon="pi ..."; dialog sizing routes through
the OnmsDialog width prop (min(Npx, 95vw)); drop maximizable, which the dialog
seam does not expose. Removes reliance on $attrs riding into PrimeVue.

@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 blockers:

  1. "Specific" one-off schedules are rejected on most server locales, and on en_GB only in September. runtimeDateTimeString formats the date with Locale.ROOT, then requires Locale.getDefault() to parse that string back, else it throws. Running the exact check against JDK CLDR data:

de-DE 0/12 fr-FR 0/12 pt-BR 0/12 sv-SE 0/12 ja-JP 0/12 zh-CN 0/12 it-IT 4/12 es-ES 7/12 id-ID 8/12 nl-NL 9/12 en-GB 11/12 (Sep fails: en-GB abbreviates "Sept")

So on a plain LANG=en_GB server, adding one-off coverage in September fails with a message blaming the server locale while every other month works; on a German or Japanese server it never works. The javadoc's claim that "the check is locale-level, not month-level, so the same request never flips between accept and reject" is wrong, because it formats the actual date. The root cause is BasicScheduleUtils.setOutCalTime doing new SimpleDateFormat(FORMAT1) with no locale, while WebSchedEntry:58 has written Locale.ROOT strings all along: passing Locale.ROOT there fixes both this guard and the pre-existing silent-ignore bug this PR correctly diagnosed. The ITs never pin Locale.getDefault(), so the guard is untested and the suite's outcome depends on the JVM locale plus the fixture month (June, which en_GB happens to accept).

  1. Changing a role's membership group orphans its schedules. validateDtoFields checks schedule users against the group only when dto.getSchedules() != null, and RoleEditorDialog.save() sends membership-group while deliberately omitting schedule. GroupManager.getUserSchedulesForRole and getRoleScheduleEntries never filter by membership, so after a group change notifd keeps paging users who are no longer in the group and the new group's members get nothing, silently. testPartialIdentityUpdates changes the group on a role that has no schedules, so it
    misses this.

Address the review blockers on On-Call Roles:

- "Specific" one-off schedules were dropped on servers whose default locale
  abbreviates months differently from Locale.ROOT (the locale WebSchedEntry
  writes with). BasicScheduleUtils.setOutCalTime parsed with the JVM default
  locale, so a round-trip through it failed on e.g. a German or Japanese
  server and silently discarded the entry. Parse with Locale.ROOT to match
  the writer; a locale-pinned unit test covers it.
- Changing a role's membership group orphaned its schedules. validateDtoFields
  only checked schedule users against the group when the request carried
  schedules, but the editor changes the group without resubmitting them, so
  retained schedules kept paging users no longer in the group. Re-validate the
  retained schedules against the new group (and drop the "already on the role"
  exemption when the group changes), rejecting the change with a clear message.
  Covered by a new IT.

Also bring the On-Call Roles dialogs onto the current @opennms/onms-ui seam:
drop primevue IftaLabel for FormField's label/for, replace the primevue
Message banners with a themed role=alert element, and bind OnmsSelect via its
inputId prop. No page imports an un-wrapped PrimeVue primitive; pnpm lint clean.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill — both blockers addressed:

"Specific" one-offs dropped on most server locales. Fixed at the root you pointed to: BasicScheduleUtils.setOutCalTime built new SimpleDateFormat(FORMAT1) with no locale, so it parsed the Locale.ROOT month WebSchedEntry writes against Locale.getDefault(). Both formatters now use Locale.ROOT, so the round-trip is locale-independent (this also clears the pre-existing silent-ignore path). Added a BasicScheduleUtilsTest case that pins Locale.setDefault(Locale.GERMANY) and asserts a ROOT-formatted dd-MMM-yyyy string still parses — it fails without the fix.

Membership-group change orphaned schedules. validateDtoFields only checked schedule users against the group when the request carried schedules, but the editor changes the group without resubmitting them. Now, when the membership group changes, the retained schedules are re-validated against the new group (and the "already on the role" exemption is dropped for that case), so a change that would strand a scheduled user is rejected with a clear message instead of silently mis-paging. New IT changes the group on a role whose schedule user isn't in the new group and asserts 400, then succeeds against a group that does contain them.

BasicScheduleUtilsTest and OnCallRolesRestServiceIT green. Also brought the three dialogs onto the current @opennms/onms-ui seam — FormField label/for in place of IftaLabel, a themed role=alert banner in place of primevue/message, and OnmsSelect bound through its inputId prop — so no dialog imports an un-wrapped PrimeVue primitive; eslint clean, vitest + vue-tsc build pass.

@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill friendly ping — both blockers are pushed: the Locale.ROOT schedule parsing fix (with a locale-pinned test) and the membership-group guard that rejects a change orphaning retained schedules. BasicScheduleUtilsTest and OnCallRolesRestServiceIT are green. Ready for another pass.

@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.

One blocker: The locale guard contradicts this PR's own fix and rejects valid requests. runtimeDateTimeString re-parses the stored timestamp with Locale.getDefault() and 400s if that fails, justified by a javadoc saying "the runtime parses stored dd-MMM-yyyy strings with the JVM default locale" — which this same PR changes to Locale.ROOT (and adds testSetOutCalTimeParsesRootLocaleMonthUnderForeignDefaultLocale to prove it). OpenNMS doesn't pin user.language, so on a de_DE server every specific schedule in March, May, October and December is rejected, and on fr_FR essentially all of them — with a message ("specific schedules would be ignored at runtime") that is no longer true. The guard's own comment claims it is "locale-level, not month-level"; it is month-level in effect.

Deleting it is the preferred path I think.

# Conflicts:
#	opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
#	ui/src/main/router/index.ts
#	ui/src/services/index.ts
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill @synqotik — rebased on current develop (the shared router/services/security-config unions plus the icon imports develop migrated to the @opennms/onms-ui/icons/* seam). Both review blockers remain addressed (Locale.ROOT schedule parsing + the membership-group orphan guard). eslint, vue-tsc build, and the OnCall tests are green — ready for another pass.

@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.

The locale guard runtimeDateTimeString (OnCallRolesRestService.java:601) is unfixed, and commit 6e7d152 made it worse. That commit changed BasicScheduleUtils.setOutCalTime to parse with Locale.ROOT — the correct fix — but left the guard that formats with Locale.ROOT and then demands Locale.getDefault() parse it back.

  • The guard now rejects requests the runtime handles perfectly, with a message ("specific schedules would be ignored at runtime") that commit 6e7d152 itself made false. The javadoc on both runtimeDateTimeString and validateTime still asserts "parses with the JVM default locale", contradicting BasicScheduleUtils.java:174 in the same PR.
  • OnCallRolesRestServiceIT now fails on 8 of the 12 locales above. Every fixture uses June (15-Jun-2093, 5-Jun-2093), and no test pins Locale.getDefault(), so testCreateAndGetRole, testCalendar, testCanonicalization, testMembershipChangeRejected… and friends 400 instead of 201 on any de/fr/it/pt/ja/zh/sv/ru JVM. CI passes only because the agents are en_US.

Delete runtimeDateTimeString and format with Locale.ROOT directly; the BasicScheduleUtils change is the whole fix. Then add a locale-pinned IT so the ROOT contract is actually covered on the API side, not just in BasicScheduleUtilsTest.

So still one blocker with this one.

The previous smoke-test-core run timed out with zero test failures
(infrastructure); re-trigger the full pipeline with smoke.
…x !smoke

runtimeDateTimeString formatted with Locale.ROOT and then demanded the JVM
default locale parse the result back — after BasicScheduleUtils moved to
Locale.ROOT parsing, the guard only rejected requests the runtime handles
fine, on any non-English JVM, with a message that fix made false. Specific
schedules now canonicalize with Locale.ROOT directly, the javadoc says so,
and a locale-pinned IT (Locale.GERMANY) covers the ROOT contract on the API
side rather than only in BasicScheduleUtilsTest.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill you were right — the guard was the bug, not the safety net. runtimeDateTimeString is deleted in 61ec64395b3: specific schedules canonicalize with Locale.ROOT directly (the BasicScheduleUtils change is the whole fix), the javadoc on validateTime now states the ROOT contract, and testSpecificScheduleIsLocaleIndependent pins Locale.GERMANY around a full create/read/delete so the contract is covered on the API side. OnCallRolesRestServiceIT 18/18 locally. Push carries !smoke.

@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.

Still a few more items:

  • OnCallRolesRestService.java:140 — the calendar GET returns 400 with an internal message. TimeInterval's constructor throws IllegalArgumentException("start (…) must come strictly before end (…)") when start >= end, and getInterval builds both endpoints from one reference day, so any stored overnight entry throws out of getRoleScheduleEntries. serverError maps all IllegalArgumentException to 400, so a valid GET is blamed on the caller and the raw message is returned. toDto handles the same exception properly via scheduleError — the pattern was recognized for the list and missed for the calendar.
  • OnCallRolesRestService.java:464 — a year outside 1000–2999 passes validation, then breaks the marshal. groups.xsd pins begins/ends to [0-9]{2}-[A-Za-z]{3}-[12][0-9]{3}…, @ValidateUsing("groups.xsd") is on Groupinfo/Time, and getMarshallerFor sets that schema. 01-Jan-3000 clears every service check and then fails saveGroups(). Neither OnmsDatePicker sets maxDate, so it's UI-reachable.
  • OnCallRolesRestService.java::84 — INVALID_NAME doesn't block C0 control chars. Java \s is only [ \t\n\x0B\f\r], so \u0001 passes validateName and then fails the XML marshal.
  • OnCallRolesRestService.java::138 — one calendar request re-checks groups.xml 28–31 times, because getRoleScheduleEntries starts with its own update(). A mid-request hand-edit also splits the month across two configs, and a role removed mid-loop NPEs at GroupManager.java:676.
  • OnCallRolesRestService.java::495 — overnight shifts are unexpressible with no hint. The rejection is correct (the runtime throws rather than wrapping), but a 20:00–06:00 night shift is the canonical on-call case and the message never says to split it in two, unlike the sibling destination-path validator.
  • The non-admin test skips PUT, /rename, and /calendar — the three most sensitive methods.

…coverage !smoke

The calendar endpoint now loads the role once and resolves every day from
that snapshot (new GroupManager overload skipping the per-call update()),
and an entry the interval math cannot evaluate - e.g. a hand-edited
overnight window - marks the response with schedule-error instead of
turning a valid GET into a 400 with an internal message. The dialog shows
that as a warning banner.

Validation now rejects years outside groups.xsd's 1000-2999 before the
marshal can fail, blocks C0 control characters in role names, and the
overnight-shift rejection explains splitting into two entries. The date
pickers pin the same range. The non-admin IT covers PUT, rename, and
calendar; new ITs cover the overnight tolerance, the year guard, the
control-character guard, and the split hint. 22 ITs green locally.
@joseanesONMS

Copy link
Copy Markdown
Contributor Author

@marshallmassengill all six addressed in 06640b4d5e2:

  1. Calendar 400 on overnight entries — the day loop now catches resolution failures per day and sets a schedule-error field on the 200 response (the toDto/scheduleError pattern, applied to the calendar); the dialog renders it as a warning banner. New IT stores a hand-edited 20:00–06:00 daily entry and asserts the calendar stays 200 with the flag.
  2. Year range — validation now rejects years outside 1000–2999 (groups.xsd's [12][0-9]{3}) before the marshal can fail, and both OnmsDatePickers pin minDate/maxDate to the same range with an inline field error. IT: 01-Jan-3000 → 400, role not created.
  3. Control charactersINVALID_NAME gains \p{Cntrl}; IT covers \u0001 on create and rename.
  4. Repeated update() per calendarGroupManager gets a getRoleScheduleEntries(Role, start, end) overload without the freshness check (the string overload delegates to it), and the calendar loads the role once inside the lock — one config check per request, one consistent snapshot, and no NPE window for a role deleted mid-request.
  5. Overnight hint — the rejection now says to add two entries (ending 23:59:59 / starting 00:00:00); IT asserts the hint text.
  6. Non-admin coverage — the 403 test now exercises PUT, POST /rename, and GET /calendar too.

OnCallRolesRestServiceIT is 22/22 locally (vs Postgres), BasicScheduleUtilsTest/GroupManagerRenameTest green, UI lint/build clean.

…a/NMS-20108-oncall-roles-pr

# Conflicts:
#	opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
#	ui/src/main/router/index.ts
#	ui/src/services/index.ts
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