diff --git a/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md b/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md index 97478210..9f11baa6 100644 --- a/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md +++ b/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md @@ -26,7 +26,7 @@ In Board Only mode, General, Board, and Tasks are marked as the primary path. Ev - `SettingsLocalNav` is reserved for related sections on a long page. It uses in-page anchors, remains horizontally scrollable at narrow widths, and does not promote subsections into global destinations. - `SettingsHelpText`, `SettingsErrorText`, `SettingsUnit`, and `SettingsActionGroup` standardize secondary explanation, validation, numeric units, routine transfer actions, and destructive actions. -General, Board, Tasks, and Data are the reference migrations. Data demonstrates continuous local navigation; simpler pages omit it. Agents extends that pattern with anchored Providers, Compatibility, Profiles, Health, and Policies workflows. Notifications, Multi-user, and Maintenance still require dedicated workflow decomposition because each combines several independent operational surfaces. +General, Board, Tasks, and Data are the reference migrations. Data demonstrates continuous local navigation; simpler pages omit it. Agents extends that pattern with anchored Providers, Compatibility, Profiles, Health, and Policies workflows. Notifications uses Health, Buzz, Reply Adapters, Delivery, and Preferences to separate diagnostics and sensitive delivery configuration from routine choices. Multi-user and Maintenance still require dedicated workflow decomposition because each combines several independent operational surfaces. ## Focus and responsive behavior diff --git a/web/src/__tests__/settings-tabs-mantine-controls.test.tsx b/web/src/__tests__/settings-tabs-mantine-controls.test.tsx index 7728698d..7ccdb285 100644 --- a/web/src/__tests__/settings-tabs-mantine-controls.test.tsx +++ b/web/src/__tests__/settings-tabs-mantine-controls.test.tsx @@ -425,9 +425,17 @@ describe('Settings tab Mantine controls', () => { it('renders Notifications text and select controls through direct Mantine primitives', async () => { const { container } = renderWithProviders(); + expect(screen.getByRole('heading', { name: 'Notifications' })).toBeDefined(); + expect( + screen.getByRole('navigation', { name: 'Notification settings sections' }) + ).toBeDefined(); + expect(screen.getByRole('link', { name: 'Reply Adapters' }).getAttribute('href')).toBe( + '#notifications-replies' + ); + expect(container.querySelectorAll('[data-settings-section]')).toHaveLength(5); expect(await screen.findByText('Communication Health')).toBeDefined(); expect(screen.getByText('Local Squad Chat')).toBeDefined(); - expect(screen.getByText('Human Reply Adapter')).toBeDefined(); + expect(screen.getByRole('heading', { name: 'Reply adapters' })).toBeDefined(); expect(screen.getByText('Buzz Connection')).toBeDefined(); expect(screen.getByLabelText(/Relay HTTP URL/)).toBeDefined(); expect(screen.getByLabelText(/Relay WebSocket URL/)).toBeDefined(); diff --git a/web/src/components/settings/tabs/NotificationsTab.tsx b/web/src/components/settings/tabs/NotificationsTab.tsx index 5bc791cc..2f21a686 100644 --- a/web/src/components/settings/tabs/NotificationsTab.tsx +++ b/web/src/components/settings/tabs/NotificationsTab.tsx @@ -15,7 +15,14 @@ import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useFeatureSettings, useDebouncedFeatureUpdate } from '@/hooks/useFeatureSettings'; import { DEFAULT_FEATURE_SETTINGS } from '@veritas-kanban/shared'; -import { SettingRow, ToggleRow, SectionHeader, SaveIndicator } from '../shared'; +import { + SettingRow, + ToggleRow, + SaveIndicator, + SettingsLocalNav, + SettingsPage, + SettingsSection, +} from '../shared'; import { api, type CommunicationAdapterInput, @@ -304,9 +311,26 @@ export function NotificationsTab() { }; return ( -
-
- + + + + -
+ -
+ + + - - -
- -
- -

- Route approved external replies back into Squad Chat threads with attribution and audit -

+ @@ -562,15 +588,135 @@ export function NotificationsTab() { )} -
+ -
+ + updateSquadWebhook('enabled', v)} + /> + {(settings.squadWebhook?.enabled ?? DEFAULT_FEATURE_SETTINGS.squadWebhook.enabled) && ( + <> + + value && updateSquadWebhook('mode', value)} - data={[ - { value: 'webhook', label: 'Generic Webhook' }, - { value: 'openclaw', label: 'OpenClaw Direct' }, - ]} - aria-label="Mode" - placeholder="Select mode" - allowDeselect={false} - size="xs" - w={192} - /> - - - {webhookMode === 'webhook' && ( - <> - - updateSquadWebhook('url', e.target.value)} - placeholder="https://example.com/webhook" - aria-label="Webhook URL" - size="xs" - w={384} - type="url" - /> - - - updateSquadWebhook('secret', e.target.value || undefined)} - placeholder="your-secret-key" - aria-label="Secret (Optional)" - size="xs" - w={256} - type="password" - /> - - - )} - - {webhookMode === 'openclaw' && ( - <> - - updateSquadWebhook('openclawGatewayUrl', e.target.value)} - placeholder="http://127.0.0.1:18789" - aria-label="Gateway URL" - size="xs" - w={384} - type="url" - /> - - - updateSquadWebhook('openclawGatewayToken', e.target.value)} - placeholder="your-gateway-token" - aria-label="Gateway Token" - size="xs" - w={384} - type="password" - /> - - - )} - - updateSquadWebhook('notifyOnHuman', v)} - /> - updateSquadWebhook('notifyOnAgent', v)} - /> - - )} -
-
-
+ + ); }