Skip to content

Feat/system notifications#228

Merged
reecebedding merged 8 commits into
mainfrom
feat/system-notifications
Apr 28, 2026
Merged

Feat/system notifications#228
reecebedding merged 8 commits into
mainfrom
feat/system-notifications

Conversation

@reecebedding
Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings April 27, 2026 09:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds UI support for system notification management, including a new admin Notifications page, navigation/route wiring, and updated user preferences behavior to respect notification provider availability.

Changes:

  • Introduces NotificationsView under Admin to list providers and configure notification destinations (add/remove).
  • Adds route + left navigation link for the new admin Notifications page.
  • Updates Preferences to disable notification alert channels when corresponding providers are unavailable, and adds new tests for the Notifications view.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/views/admin/NotificationsView.vue New admin view to display providers and manage notification destinations for predefined notifications.
src/views/tests/NotificationsView.spec.ts Adds unit tests covering provider display, destination add/remove flows, and Slack-unavailable behavior.
src/views/tests/LeftSideNav.spec.ts Updates admin link ordering assertions to include the new Notifications link and Import placement.
src/views/PreferencesView.vue Fetches provider availability and uses it to disable/clean up selectable notification channels.
src/views/LeftSideNav.vue Adds “Notifications” to the admin navigation links list.
src/router/index.ts Registers the /admin/notifications route pointing to the new admin Notifications view.
Comments suppressed due to low confidence (1)

src/views/PreferencesView.vue:599

  • syncUnavailableSlackSelections now syncs all notification channel availability (email + slack) via removeUnavailableNotificationSelections(), so the function name is misleading. Renaming it (and the related hadUnavailableSlackSelection variable) would make the intent clearer and avoid future confusion.
const syncUnavailableSlackSelections = async () => {
  if (!removeUnavailableNotificationSelections()) {
    return;
  }

  await updateNotificationPreferences({ silent: true });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const wrapper = mount(NotificationsView);

expect(wrapper.text()).toContain('Providers');
expect(wrapper.text()).toContain('Email');
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The view renders provider.providerType (e.g. email / slack) in the Providers section, so this assertion for Email is likely to fail. Either update the expectation to match the rendered providerType text, or adjust the component to render provider.displayName if that’s the intended UI.

Suggested change
expect(wrapper.text()).toContain('Email');
expect(wrapper.text()).toContain('email');

Copilot uses AI. Check for mistakes.
Comment on lines 46 to +59
const systemUsersIndex = linkTexts.indexOf('System Users');
const agentsIndex = linkTexts.indexOf('Agents');
const notificationsIndex = linkTexts.indexOf('Notifications');
const risksIndex = linkTexts.indexOf('Risks');
const subjectTemplatesIndex = linkTexts.indexOf('Subject Templates');
const riskTemplatesIndex = linkTexts.indexOf('Risk Templates');
const importIndex = linkTexts.indexOf('Import');

expect(systemUsersIndex).toBeGreaterThanOrEqual(0);
expect(agentsIndex).toBeGreaterThan(systemUsersIndex);
expect(risksIndex).toBeGreaterThan(agentsIndex);
expect(subjectTemplatesIndex).toBeGreaterThan(risksIndex);
expect(riskTemplatesIndex).toBeGreaterThan(subjectTemplatesIndex);
expect(notificationsIndex).toBeGreaterThan(riskTemplatesIndex);
expect(importIndex).toBeGreaterThan(notificationsIndex);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

This ordering test can produce false positives because indexOf() returns -1 for missing links, and several indices (e.g. notificationsIndex, risksIndex, importIndex) are never asserted to be >= 0. Consider asserting existence for each expected link, and re-adding an explicit ordering check between Agents and Risks (it was removed), so the test actually validates the full admin section ordering.

Copilot uses AI. Check for mistakes.
Comment on lines +691 to +703
const [userResponse, subscriptionResponse, notificationProvidersResponse] =
await Promise.all([
axios.get<{ data: CCFUser }>('/api/users/me'),
axios.get<{ data: SubscriptionsPreferencesResponse }>(
'/api/users/me/subscriptions',
),
axios.get<{
data: Array<{
providerType?: string;
enabled?: boolean;
}>;
}>('/api/notifications/providers'),
]);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

loadUserData() now uses Promise.all([... , axios.get('/api/notifications/providers')]), so any failure in the providers request will throw and set loadError, preventing the preferences page from loading even if /api/users/me and /api/users/me/subscriptions succeeded. To avoid making preferences availability depend on this auxiliary endpoint, consider fetching providers with Promise.allSettled or a nested try/catch and falling back to a safe default (e.g. keep channels enabled / keep existing selections) when the providers call fails.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/views/PreferencesView.vue:606

  • syncUnavailableSlackSelections() now calls removeUnavailableNotificationSelections(), which can remove email selections as well (based on provider availability). The function name (and any related references) is now misleading; consider renaming it to reflect that it syncs all notification channel selections, not just Slack.
const syncUnavailableSlackSelections = async () => {
  if (!removeUnavailableNotificationSelections()) {
    return;
  }

  await updateNotificationPreferences({ silent: true });
};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/views/admin/NotificationsView.vue Outdated
Comment on lines +255 to +258
function isProviderDisabled(
provider: NotificationDestinationProvider,
): boolean {
return provider === 'slack' && !canSelectSlackDestination.value;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The provider select only disables Slack based on Slack-link configuration (isProviderDisabled ignores the backend provider enabled flag). This allows configuring destinations for providers that the backend reports as disabled, which is inconsistent with Providers section and with PreferencesView disabling channels based on /api/notifications/providers. Consider also disabling (and explaining) providers whose NotificationProviderInfo.enabled is false.

Suggested change
function isProviderDisabled(
provider: NotificationDestinationProvider,
): boolean {
return provider === 'slack' && !canSelectSlackDestination.value;
function getProviderInfo(
provider: NotificationDestinationProvider,
): NotificationProviderInfo | undefined {
return notificationProviders.value.find(
(providerInfo) => providerInfo.providerType === provider,
);
}
function isProviderDisabled(
provider: NotificationDestinationProvider,
): boolean {
if (provider === 'slack' && !canSelectSlackDestination.value) {
return true;
}
return getProviderInfo(provider)?.enabled === false;

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +286
function getApiErrorMessage(error: unknown, fallbackMessage: string): string {
const axiosError = error as AxiosError<ErrorResponse<ErrorBody>>;
return axiosError.response?.data?.errors?.body || fallbackMessage;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

getApiErrorMessage() unconditionally casts to AxiosError and only reads response.data.errors.body, otherwise returning the fallback. In other admin views (e.g., Agents/RiskTemplates), the helper first checks isAxiosError(error) and then falls back to error.message when available, which produces much more useful errors for network/JS failures. Consider aligning this helper with that established pattern.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +381 to +398
async function loadSlackDestinationStatus() {
slackDestinationStatusLoading.value = true;

try {
await axios.get('/api/auth/slack/link/status');
slackDestinationConfigured.value = true;
} catch (error) {
const errorResponse = error as AxiosError<ErrorResponse<ErrorBody>>;

if (errorResponse.response?.status === 404) {
slackDestinationConfigured.value = false;
return;
}

slackDestinationConfigured.value = true;
} finally {
slackDestinationStatusLoading.value = false;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

loadSlackDestinationStatus() treats any non-404 error as “Slack configured” and does not surface the failure. This can enable Slack destinations when the status check is failing (e.g., 500/network), and gives no feedback to the admin. Consider mirroring the existing Slack status loader behavior (e.g., show an error toast / store an error message, and/or keep Slack destinations disabled when the status is unknown).

Copilot uses AI. Check for mistakes.
Comment thread src/views/PreferencesView.vue Outdated
Comment on lines 600 to 604
const syncUnavailableSlackSelections = async () => {
if (!removeUnavailableSlackSelections()) {
if (!removeUnavailableNotificationSelections()) {
return;
}

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

syncUnavailableSlackSelections now calls removeUnavailableNotificationSelections() and can remove non-Slack channels (e.g., email) based on provider availability. Renaming this helper to reflect the broader responsibility (and updating any related identifiers) would improve clarity for future maintenance.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@reecebedding reecebedding merged commit 1e7485a into main Apr 28, 2026
6 checks passed
@reecebedding reecebedding deleted the feat/system-notifications branch April 28, 2026 09:19
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.

3 participants