@burdenoff/microfe-channel 2026.513.2 → 2026.514.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/ChannelRoot.js +1 -1
- package/dist/channel/ChannelRoot.js.map +1 -1
- package/dist/channel/hooks/useChannelSubscription.d.ts +4 -4
- package/dist/channel/hooks/useChannelSubscription.d.ts.map +1 -1
- package/dist/channel/hooks/useChannelSubscription.js +2 -2
- package/dist/channel/hooks/useChannelSubscription.js.map +1 -1
- package/dist/channel/hooks/useChannels.d.ts.map +1 -1
- package/dist/channel/hooks/useChannels.js +130 -124
- package/dist/channel/hooks/useChannels.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js +5 -5
- package/dist/channel/modules/channels/pages/ChannelSettingsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js +25 -7
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +7 -7
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionsListPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js +137 -133
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js +162 -158
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/EditChannelPage.js +5 -5
- package/dist/channel/modules/channels/pages/EditChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/SendMessagePage.js +2 -2
- package/dist/channel/modules/channels/pages/SendMessagePage.js.map +1 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.d.ts.map +1 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js +11 -3
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js.map +1 -1
- package/dist/translations/en.d.ts +260 -0
- package/dist/translations/en.d.ts.map +1 -0
- package/package.json +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChannels.js","names":[],"sources":["../../../src/channel/hooks/useChannels.ts"],"sourcesContent":["/**\n * Channel Hooks\n * GraphQL queries and mutations for channel management\n *\n * All types are derived from codegen-generated types (src/generated/global-types.ts)\n * and operation result types (src/generated/global-operations.ts).\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { getGlobalChannelClient } from '../graphql/global-client';\nimport { useChannel as useChannelContext } from '../providers/ChannelProvider';\nimport { emitShellEvent } from '../utils/shellBridge';\nimport {\n GetChannelsDocument,\n GetChannelDocument,\n CreateChannelDocument,\n UpdateChannelDocument,\n DeleteChannelDocument,\n ActivateChannelDocument,\n DeactivateChannelDocument,\n RunChannelHealthCheckDocument,\n CreateConnectionDocument,\n UpdateConnectionDocument,\n DeleteConnectionDocument,\n TestConnectionHealthDocument,\n DeliverNotificationDocument,\n GetDeliveryAttemptsDocument,\n GetWebhookEventsDocument,\n GetUserPlatformConnectionsDocument,\n RetryDeliveryDocument,\n GetDeliveryStatsDocument,\n type GetChannelsQuery,\n type GetChannelQuery,\n type CreateChannelMutation,\n type UpdateChannelMutation,\n type RunChannelHealthCheckMutation,\n type CreateConnectionMutation,\n type UpdateConnectionMutation,\n type TestConnectionHealthMutation,\n type DeliverNotificationMutation,\n type GetDeliveryAttemptsQuery,\n type GetWebhookEventsQuery,\n type GetUserPlatformConnectionsQuery,\n type RetryDeliveryMutation,\n type GetDeliveryStatsQuery,\n} from '../../generated/global-operations';\n\n/** Log an operational error — console only, never escalate to shell error boundary. */\nfunction report(error: unknown, action: string, context: Record<string, unknown> = {}) {\n console.error(`[channel] ${action}`, error, context);\n}\n\n// ============================================================================\n// TYPES — derived from codegen, exposed as string unions for convenience.\n//\n// Codegen generates TypeScript enums (e.g., ChannelPlatform.Email = 'EMAIL').\n// Pages/components use string literals ('EMAIL', 'ACTIVE', etc.) which are\n// not directly assignable to TS enums. We re-export the underlying string\n// union types so consumers can use plain strings without casting.\n// ============================================================================\n\nimport type {\n ChannelPlatform as _ChannelPlatform,\n ChannelStatus as _ChannelStatus,\n ChannelAuthType as _ChannelAuthType,\n ConnectionHealth as _ConnectionHealth,\n AdapterType as _AdapterType,\n DeliveryPriority as _DeliveryPriority,\n DeliveryStatus as _DeliveryStatus,\n ChannelFilterInput as _ChannelFilterInput,\n CreateChannelInput as _CreateChannelInput,\n UpdateChannelInput as _UpdateChannelInput,\n CreateConnectionInput as _CreateConnectionInput,\n UpdateConnectionInput as _UpdateConnectionInput,\n ChannelHealthCheck as _ChannelHealthCheck,\n ConnectionTestResult as _ConnectionTestResult,\n ChannelDeliveryResult as _ChannelDeliveryResult,\n ChannelDeliveryError as _ChannelDeliveryError,\n ChannelDeliveryContentInput as _ChannelDeliveryContentInput,\n ChannelDeliveryInput as _ChannelDeliveryInput,\n BulkChannelDeliveryResult as _BulkChannelDeliveryResult,\n UserPresenceStatus as _UserPresenceStatus,\n PairingStatus as _PairingStatus,\n WebhookProcessingStatus as _WebhookProcessingStatus,\n} from '../../generated/global-types';\n\n// ── Enum-like string unions (backward-compatible with string literal usage) ──\n\n/** Communication platform — extends generated enum with values pending backend schema update */\nexport type ChannelPlatform = `${_ChannelPlatform}` | 'INAPP';\n/** Channel lifecycle status */\nexport type ChannelStatus = `${_ChannelStatus}`;\n/** Authentication method for connections */\nexport type ChannelAuthType = `${_ChannelAuthType}`;\n/** Connection health status */\nexport type ConnectionHealth = `${_ConnectionHealth}`;\n/** Specific adapter/provider type — extends generated enum with values pending backend schema update */\nexport type AdapterType =\n | `${_AdapterType}`\n | 'INAPP_NATIVE'\n | 'NOVU'\n | 'KNOCKLABS'\n | 'SENDGRID'\n | 'MAILGUN'\n | 'POSTMARK'\n | 'MANDRILL'\n | 'RESEND'\n | 'SMTP'\n | 'VONAGE'\n | 'PLIVO'\n | 'MESSAGEBIRD'\n | 'INFOBIP'\n | 'APNS'\n | 'ONESIGNAL'\n | 'EXPO'\n | 'WEBCHAT_NATIVE'\n | 'SLACK_WEBHOOK'\n | 'TEAMS_WEBHOOK'\n | 'DISCORD_WEBHOOK';\n/** Delivery priority level */\nexport type DeliveryPriority = `${_DeliveryPriority}`;\n/** Message delivery status */\nexport type DeliveryStatus = `${_DeliveryStatus}`;\n/** User presence status on a platform */\nexport type UserPresenceStatus = `${_UserPresenceStatus}`;\n/** Pairing status for Bundle-2 flow */\nexport type PairingStatus = `${_PairingStatus}`;\n/** Webhook event processing status */\nexport type WebhookProcessingStatus = `${_WebhookProcessingStatus}`;\n\n// ── Utility: recursively unwrap codegen enums to string unions ──────────────\n// Codegen generates TS enums that are not assignable from string literals.\n// This mapped type converts `ChannelPlatform` (enum) → `'EMAIL' | 'SMS' | ...`\n// at every nested level of a type, making all Input/Result types compatible\n// with plain string usage.\n\ntype EnumToString<T> = [T] extends [null]\n ? null\n : T extends null | undefined\n ? EnumToString<NonNullable<T>> | Extract<T, null | undefined>\n : T extends _ChannelPlatform\n ? ChannelPlatform\n : T extends _ChannelStatus\n ? ChannelStatus\n : T extends _ChannelAuthType\n ? ChannelAuthType\n : T extends _ConnectionHealth\n ? ConnectionHealth\n : T extends _AdapterType\n ? AdapterType\n : T extends _DeliveryPriority\n ? DeliveryPriority\n : T extends _DeliveryStatus\n ? DeliveryStatus\n : T extends _UserPresenceStatus\n ? UserPresenceStatus\n : T extends _PairingStatus\n ? PairingStatus\n : T extends _WebhookProcessingStatus\n ? WebhookProcessingStatus\n : T extends Array<infer U>\n ? Array<EnumToString<U>>\n : T extends object\n ? { [K in keyof T]: EnumToString<T[K]> }\n : T;\n\n// ── Input types (used by mutations) ─────────────────────────────────────────\n\nexport type CreateChannelInput = EnumToString<_CreateChannelInput>;\nexport type UpdateChannelInput = EnumToString<_UpdateChannelInput>;\nexport type CreateConnectionInput = EnumToString<_CreateConnectionInput>;\nexport type UpdateConnectionInput = EnumToString<_UpdateConnectionInput>;\nexport type ChannelFilterInput = EnumToString<_ChannelFilterInput>;\nexport type ChannelDeliveryContentInput = EnumToString<_ChannelDeliveryContentInput>;\nexport type ChannelDeliveryInput = EnumToString<_ChannelDeliveryInput>;\nexport type {\n ChannelPaginationInput,\n ChannelDeliveryOptionsInput,\n ChannelRetryPolicyInput,\n BulkChannelDeliveryInput,\n} from '../../generated/global-types';\n\n// ── Schema result types ─────────────────────────────────────────────────────\n\nexport type ChannelHealthCheck = EnumToString<_ChannelHealthCheck>;\nexport type ConnectionTestResult = EnumToString<_ConnectionTestResult>;\nexport type ChannelDeliveryResult = EnumToString<_ChannelDeliveryResult>;\nexport type ChannelDeliveryError = EnumToString<_ChannelDeliveryError>;\nexport type BulkChannelDeliveryResult = EnumToString<_BulkChannelDeliveryResult>;\n\n// ── Derived types from query results ──────────────────────────────────────\n// These represent the actual shape of data returned by our GraphQL operations,\n// not the full schema types (which include __typename, nested relations, etc.)\n\n/** Channel as returned by GetChannels query (list item) */\nexport type Channel = NonNullable<GetChannelsQuery['getChannels']['items'][number]>;\n\n/** Channel as returned by GetChannel query (full detail) */\nexport type ChannelDetail = NonNullable<GetChannelQuery['getChannel']>;\n\n/** ChannelConnection from the GetChannel query (full connection detail) */\nexport type ChannelConnection = NonNullable<ChannelDetail['connection']>;\n\n/** ChannelList response shape */\nexport type ChannelList = GetChannelsQuery['getChannels'];\n\n/** Message delivery attempt as returned by GetDeliveryAttempts query */\nexport type DeliveryAttempt = NonNullable<\n GetDeliveryAttemptsQuery['getChannelDeliveryAttempts']['items'][number]\n>;\n\n/** Webhook event as returned by GetWebhookEvents query */\nexport type WebhookEvent = NonNullable<\n GetWebhookEventsQuery['getChannelWebhookEvents']['items'][number]\n>;\n\n/** User platform connection as returned by GetUserPlatformConnections query */\nexport type UserPlatformConnection = NonNullable<\n GetUserPlatformConnectionsQuery['listUserPlatformConnectionsByChannel']['items'][number]\n>;\n\n/** Delivery statistics as returned by GetDeliveryStats query */\nexport type DeliveryStats = NonNullable<GetDeliveryStatsQuery['getDeliveryStats']>;\n\n// ============================================================================\n// HOOKS\n// ============================================================================\n\n/**\n * Hook for managing channels list\n */\nexport function useChannels(initialFilter?: ChannelFilterInput) {\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n const [channels, setChannels] = useState<Channel[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [hasMore, setHasMore] = useState(false);\n const [total, setTotal] = useState(0);\n const [filter, setFilter] = useState<ChannelFilterInput>(initialFilter ?? {});\n\n // Get Apollo client with auth token and organization context\n const getClient = useCallback(() => {\n return getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n }, [apiGatewayUrl, authToken, organizationId]);\n\n // Fetch channels\n const fetchChannels = useCallback(\n async (pagination?: { offset?: number; limit?: number }) => {\n setIsLoading(true);\n setError(null);\n\n try {\n const client = getClient();\n const result = await client.query<GetChannelsQuery>({\n query: GetChannelsDocument,\n variables: {\n filter,\n pagination: pagination ?? { offset: 0, limit: 50 },\n },\n fetchPolicy: 'network-only',\n });\n\n const data = result.data?.getChannels;\n if (data) {\n setChannels(data.items);\n setTotal(data.total);\n setHasMore(data.hasMore);\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch channels';\n report(err, 'Failed to fetch channels');\n setError(message);\n } finally {\n setIsLoading(false);\n }\n },\n [getClient, filter]\n );\n\n // Initial fetch — defer to microtask to avoid synchronous setState in effect\n useEffect(() => {\n void Promise.resolve().then(() => fetchChannels());\n }, [fetchChannels]);\n\n // Create channel\n const createChannel = useCallback(\n async (input: CreateChannelInput): Promise<Channel | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<CreateChannelMutation>({\n mutation: CreateChannelDocument,\n variables: { input },\n });\n const created = (result.data?.createChannel as Channel) ?? null;\n if (created) {\n emitShellEvent('channel.created', { id: created.id, platform: created.platform });\n }\n await fetchChannels();\n return created;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to create channel';\n report(err, 'Failed to create channel');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Update channel\n const updateChannel = useCallback(\n async (id: string, input: UpdateChannelInput): Promise<Channel | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<UpdateChannelMutation>({\n mutation: UpdateChannelDocument,\n variables: { id, input },\n });\n emitShellEvent('channel.updated', { id });\n await fetchChannels();\n return (result.data?.updateChannel as Channel) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update channel';\n report(err, 'Failed to update channel');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Delete channel\n const deleteChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeleteChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.deleted', { id });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to delete channel';\n report(err, 'Failed to delete channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Activate channel\n const activateChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: ActivateChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.updated', { id, action: 'activated' });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to activate channel';\n report(err, 'Failed to activate channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Deactivate channel\n const deactivateChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeactivateChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.updated', { id, action: 'deactivated' });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to deactivate channel';\n report(err, 'Failed to deactivate channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Run health check\n const runHealthCheck = useCallback(\n async (id: string): Promise<ChannelHealthCheck | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<RunChannelHealthCheckMutation>({\n mutation: RunChannelHealthCheckDocument,\n variables: { id },\n });\n await fetchChannels();\n return (result.data?.runChannelHealthCheck as ChannelHealthCheck) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to run health check';\n report(err, 'Failed to run health check');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Update filter\n const updateFilter = useCallback((newFilter: Partial<ChannelFilterInput>) => {\n setFilter((prev) => ({ ...prev, ...newFilter }));\n }, []);\n\n // Reset filter\n const resetFilter = useCallback(() => {\n setFilter({});\n }, []);\n\n // Deliver notification via channel\n const deliverNotification = useCallback(\n async (\n input: ChannelDeliveryInput\n ): Promise<{ success: boolean; results?: ChannelDeliveryResult[]; error?: string }> => {\n try {\n const client = getClient();\n const result = await client.mutate<DeliverNotificationMutation>({\n mutation: DeliverNotificationDocument,\n variables: { input },\n });\n\n const data = result.data?.deliverNotification;\n if (data) {\n emitShellEvent('delivery.completed', {\n success: data.success ?? false,\n messageId: data.messageId,\n platform: data.platform,\n status: data.status,\n });\n return {\n success: data.success ?? false,\n results: [data as ChannelDeliveryResult],\n };\n }\n return { success: false, error: 'No data returned' };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to deliver notification';\n report(err, 'Failed to deliver notification');\n return { success: false, error: message };\n }\n },\n [getClient]\n );\n\n return {\n channels,\n isLoading,\n error,\n hasMore,\n total,\n filter,\n refetch: fetchChannels,\n createChannel,\n updateChannel,\n deleteChannel,\n activateChannel,\n deactivateChannel,\n runHealthCheck,\n updateFilter,\n resetFilter,\n deliverNotification,\n };\n}\n\n/**\n * Hook for managing a single channel\n */\nexport function useChannel(id: string | undefined) {\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n const [channel, setChannel] = useState<ChannelDetail | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Get Apollo client with auth token and organization context\n const getClient = useCallback(() => {\n return getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n }, [apiGatewayUrl, authToken, organizationId]);\n\n // Fetch channel\n const fetchChannel = useCallback(async () => {\n if (!id) {\n setChannel(null);\n setIsLoading(false);\n return;\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n const client = getClient();\n const result = await client.query<GetChannelQuery>({\n query: GetChannelDocument,\n variables: { id },\n fetchPolicy: 'network-only',\n });\n\n setChannel(result.data?.getChannel ?? null);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch channel';\n report(err, 'Failed to fetch channel');\n setError(message);\n } finally {\n setIsLoading(false);\n }\n }, [id, getClient]);\n\n useEffect(() => {\n void Promise.resolve().then(() => fetchChannel());\n }, [fetchChannel]);\n\n // Update channel\n const updateChannel = useCallback(\n async (input: UpdateChannelInput): Promise<ChannelDetail | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<UpdateChannelMutation>({\n mutation: UpdateChannelDocument,\n variables: { id, input },\n });\n emitShellEvent('channel.updated', { id });\n await fetchChannel();\n return (result.data?.updateChannel as ChannelDetail) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update channel';\n report(err, 'Failed to update channel');\n setError(message);\n return null;\n }\n },\n [id, getClient, fetchChannel]\n );\n\n // Run health check\n const runHealthCheck = useCallback(async (): Promise<ChannelHealthCheck | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<RunChannelHealthCheckMutation>({\n mutation: RunChannelHealthCheckDocument,\n variables: { id },\n });\n await fetchChannel();\n return (result.data?.runChannelHealthCheck as ChannelHealthCheck) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to run health check';\n report(err, 'Failed to run health check');\n setError(message);\n return null;\n }\n }, [id, getClient, fetchChannel]);\n\n // Create connection\n const createConnection = useCallback(\n async (input: Omit<CreateConnectionInput, 'channelId'>): Promise<ChannelConnection | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<CreateConnectionMutation>({\n mutation: CreateConnectionDocument,\n variables: { input: { ...input, channelId: id } },\n });\n const created = (result.data?.createConnection as ChannelConnection) ?? null;\n if (created) {\n emitShellEvent('connection.created', { id: created.id, channelId: id });\n }\n await fetchChannel();\n return created;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to create connection';\n report(err, 'Failed to create connection');\n setError(message);\n return null;\n }\n },\n [id, getClient, fetchChannel]\n );\n\n // Update connection\n const updateConnection = useCallback(\n async (\n connectionId: string,\n input: UpdateConnectionInput\n ): Promise<ChannelConnection | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<UpdateConnectionMutation>({\n mutation: UpdateConnectionDocument,\n variables: { id: connectionId, input },\n });\n emitShellEvent('connection.updated', { id: connectionId });\n await fetchChannel();\n return (result.data?.updateConnection as ChannelConnection) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update connection';\n report(err, 'Failed to update connection');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Delete connection\n // Returns void on success; throws on failure (never returns false).\n const deleteConnection = useCallback(\n async (connectionId: string): Promise<void> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeleteConnectionDocument,\n variables: { id: connectionId },\n update(cache) {\n cache.evict({ id: `ChannelConnection:${connectionId}` });\n cache.gc();\n },\n });\n emitShellEvent('connection.deleted', { id: connectionId });\n await fetchChannel();\n } catch (err: unknown) {\n report(err, 'Failed to delete connection');\n throw err;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Test connection\n const testConnection = useCallback(\n async (connectionId: string): Promise<ConnectionTestResult | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<TestConnectionHealthMutation>({\n mutation: TestConnectionHealthDocument,\n variables: { id: connectionId },\n });\n await fetchChannel();\n return (result.data?.testConnectionHealth as ConnectionTestResult) ?? null;\n } catch (err: unknown) {\n report(err, 'Failed to test connection');\n throw err;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Send test message via channel using deliverNotification\n const sendTestNotification = useCallback(\n async (\n recipient: string,\n message: string,\n options?: { subject?: string; html?: string }\n ): Promise<ChannelDeliveryResult | null> => {\n if (!channel) return null;\n\n try {\n const client = getClient();\n\n // Build content based on platform\n const content: ChannelDeliveryContentInput = {\n text: message,\n };\n\n // Add platform-specific content\n if (channel.platform === 'EMAIL') {\n content.subject = options?.subject ?? 'Test Notification';\n if (options?.html) {\n content.html = options.html;\n }\n } else if (channel.platform === 'PUSH') {\n content.title = options?.subject ?? 'Test Notification';\n }\n\n const input: ChannelDeliveryInput = {\n platform: channel.platform,\n organizationId: channel.organizationId,\n recipient,\n content,\n connectionId: channel.connection?.id,\n };\n\n const result = await client.mutate<DeliverNotificationMutation>({\n mutation: DeliverNotificationDocument,\n variables: { input },\n });\n\n const out = (result.data?.deliverNotification as ChannelDeliveryResult) ?? null;\n if (out) {\n emitShellEvent('delivery.completed', {\n success: out.success ?? false,\n messageId: out.messageId,\n platform: out.platform,\n status: out.status,\n test: true,\n });\n }\n return out;\n } catch (err: unknown) {\n report(err, 'Failed to send test message');\n throw err;\n }\n },\n [channel, getClient]\n );\n\n // Fetch delivery attempts\n const fetchDeliveryAttempts = useCallback(\n async (limit: number = 50): Promise<DeliveryAttempt[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetDeliveryAttemptsQuery>({\n query: GetDeliveryAttemptsDocument,\n variables: { channelId: id, limit },\n fetchPolicy: 'network-only',\n });\n return result.data?.getChannelDeliveryAttempts?.items ?? [];\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch delivery attempts';\n report(err, 'Failed to fetch delivery attempts');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Fetch webhook events\n const fetchWebhookEvents = useCallback(\n async (limit: number = 50): Promise<WebhookEvent[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetWebhookEventsQuery>({\n query: GetWebhookEventsDocument,\n variables: { channelId: id, limit },\n fetchPolicy: 'network-only',\n });\n return result.data?.getChannelWebhookEvents?.items ?? [];\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch webhook events';\n report(err, 'Failed to fetch webhook events');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Fetch user platform connections\n const fetchUserPlatformConnections = useCallback(\n async (limit: number = 50): Promise<UserPlatformConnection[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetUserPlatformConnectionsQuery>({\n query: GetUserPlatformConnectionsDocument,\n variables: { channelId: id, pagination: { limit } },\n fetchPolicy: 'network-only',\n });\n return result.data?.listUserPlatformConnectionsByChannel?.items ?? [];\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : 'Failed to fetch user platform connections';\n report(err, 'Failed to fetch user platform connections');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Retry a failed delivery\n const retryDelivery = useCallback(\n async (attemptId: string): Promise<{ success: boolean; error?: string }> => {\n try {\n const client = getClient();\n const result = await client.mutate<RetryDeliveryMutation>({\n mutation: RetryDeliveryDocument,\n variables: { attemptId },\n });\n const data = result.data?.retryDelivery;\n if (data?.success) {\n emitShellEvent('delivery.completed', {\n success: true,\n attemptId,\n messageId: data.messageId,\n retried: true,\n });\n // Refresh the delivery attempts list\n await fetchDeliveryAttempts();\n return { success: true };\n } else {\n return { success: false, error: data?.error || 'Retry failed' };\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to retry delivery';\n report(err, 'Failed to retry delivery');\n return { success: false, error: message };\n }\n },\n [getClient, fetchDeliveryAttempts]\n );\n\n // Fetch delivery statistics\n const fetchDeliveryStats = useCallback(\n async (startDate?: string, endDate?: string): Promise<DeliveryStats | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.query<GetDeliveryStatsQuery>({\n query: GetDeliveryStatsDocument,\n variables: { channelId: id, startDate, endDate },\n fetchPolicy: 'network-only',\n });\n return result.data?.getDeliveryStats ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch delivery stats';\n report(err, 'Failed to fetch delivery stats');\n setError(message);\n return null;\n }\n },\n [id, getClient]\n );\n\n return {\n channel,\n isLoading,\n error,\n refetch: fetchChannel,\n updateChannel,\n runHealthCheck,\n createConnection,\n updateConnection,\n deleteConnection,\n testConnection,\n sendTestNotification,\n fetchDeliveryAttempts,\n fetchWebhookEvents,\n fetchUserPlatformConnections,\n retryDelivery,\n fetchDeliveryStats,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useChannels.js","names":[],"sources":["../../../src/channel/hooks/useChannels.ts"],"sourcesContent":["/**\n * Channel Hooks\n * GraphQL queries and mutations for channel management\n *\n * All types are derived from codegen-generated types (src/generated/global-types.ts)\n * and operation result types (src/generated/global-operations.ts).\n */\n\nimport { useState, useCallback, useEffect } from 'react';\nimport { getGlobalChannelClient } from '../graphql/global-client';\nimport { useChannel as useChannelContext } from '../providers/ChannelProvider';\nimport { emitShellEvent, reportShellError } from '../utils/shellBridge';\nimport {\n GetChannelsDocument,\n GetChannelDocument,\n CreateChannelDocument,\n UpdateChannelDocument,\n DeleteChannelDocument,\n ActivateChannelDocument,\n DeactivateChannelDocument,\n RunChannelHealthCheckDocument,\n CreateConnectionDocument,\n UpdateConnectionDocument,\n DeleteConnectionDocument,\n TestConnectionHealthDocument,\n DeliverNotificationDocument,\n GetDeliveryAttemptsDocument,\n GetWebhookEventsDocument,\n GetUserPlatformConnectionsDocument,\n RetryDeliveryDocument,\n GetDeliveryStatsDocument,\n type GetChannelsQuery,\n type GetChannelQuery,\n type CreateChannelMutation,\n type UpdateChannelMutation,\n type RunChannelHealthCheckMutation,\n type CreateConnectionMutation,\n type UpdateConnectionMutation,\n type TestConnectionHealthMutation,\n type DeliverNotificationMutation,\n type GetDeliveryAttemptsQuery,\n type GetWebhookEventsQuery,\n type GetUserPlatformConnectionsQuery,\n type RetryDeliveryMutation,\n type GetDeliveryStatsQuery,\n} from '../../generated/global-operations';\n\n/** Forward an operational error to the shell SRE sink. */\nfunction report(error: unknown, action: string, context: Record<string, unknown> = {}) {\n reportShellError(error, { module: 'channel', context: { action, ...context } });\n}\n\n// ============================================================================\n// TYPES — derived from codegen, exposed as string unions for convenience.\n//\n// Codegen generates TypeScript enums (e.g., ChannelPlatform.Email = 'EMAIL').\n// Pages/components use string literals ('EMAIL', 'ACTIVE', etc.) which are\n// not directly assignable to TS enums. We re-export the underlying string\n// union types so consumers can use plain strings without casting.\n// ============================================================================\n\nimport type {\n ChannelPlatform as _ChannelPlatform,\n ChannelStatus as _ChannelStatus,\n ChannelAuthType as _ChannelAuthType,\n ConnectionHealth as _ConnectionHealth,\n AdapterType as _AdapterType,\n DeliveryPriority as _DeliveryPriority,\n DeliveryStatus as _DeliveryStatus,\n ChannelFilterInput as _ChannelFilterInput,\n CreateChannelInput as _CreateChannelInput,\n UpdateChannelInput as _UpdateChannelInput,\n CreateConnectionInput as _CreateConnectionInput,\n UpdateConnectionInput as _UpdateConnectionInput,\n ChannelHealthCheck as _ChannelHealthCheck,\n ConnectionTestResult as _ConnectionTestResult,\n ChannelDeliveryResult as _ChannelDeliveryResult,\n ChannelDeliveryError as _ChannelDeliveryError,\n ChannelDeliveryContentInput as _ChannelDeliveryContentInput,\n ChannelDeliveryInput as _ChannelDeliveryInput,\n BulkChannelDeliveryResult as _BulkChannelDeliveryResult,\n UserPresenceStatus as _UserPresenceStatus,\n PairingStatus as _PairingStatus,\n WebhookProcessingStatus as _WebhookProcessingStatus,\n} from '../../generated/global-types';\n\n// ── Enum-like string unions (backward-compatible with string literal usage) ──\n\n/** Communication platform — extends generated enum with values pending backend schema update */\nexport type ChannelPlatform = `${_ChannelPlatform}` | 'INAPP';\n/** Channel lifecycle status */\nexport type ChannelStatus = `${_ChannelStatus}`;\n/** Authentication method for connections */\nexport type ChannelAuthType = `${_ChannelAuthType}`;\n/** Connection health status */\nexport type ConnectionHealth = `${_ConnectionHealth}`;\n/** Specific adapter/provider type — extends generated enum with values pending backend schema update */\nexport type AdapterType =\n | `${_AdapterType}`\n | 'INAPP_NATIVE'\n | 'NOVU'\n | 'KNOCKLABS'\n | 'SENDGRID'\n | 'MAILGUN'\n | 'POSTMARK'\n | 'MANDRILL'\n | 'RESEND'\n | 'SMTP'\n | 'VONAGE'\n | 'PLIVO'\n | 'MESSAGEBIRD'\n | 'INFOBIP'\n | 'APNS'\n | 'ONESIGNAL'\n | 'EXPO'\n | 'WEBCHAT_NATIVE'\n | 'SLACK_WEBHOOK'\n | 'TEAMS_WEBHOOK'\n | 'DISCORD_WEBHOOK';\n/** Delivery priority level */\nexport type DeliveryPriority = `${_DeliveryPriority}`;\n/** Message delivery status */\nexport type DeliveryStatus = `${_DeliveryStatus}`;\n/** User presence status on a platform */\nexport type UserPresenceStatus = `${_UserPresenceStatus}`;\n/** Pairing status for Bundle-2 flow */\nexport type PairingStatus = `${_PairingStatus}`;\n/** Webhook event processing status */\nexport type WebhookProcessingStatus = `${_WebhookProcessingStatus}`;\n\n// ── Utility: recursively unwrap codegen enums to string unions ──────────────\n// Codegen generates TS enums that are not assignable from string literals.\n// This mapped type converts `ChannelPlatform` (enum) → `'EMAIL' | 'SMS' | ...`\n// at every nested level of a type, making all Input/Result types compatible\n// with plain string usage.\n\ntype EnumToString<T> = [T] extends [null]\n ? null\n : T extends null | undefined\n ? EnumToString<NonNullable<T>> | Extract<T, null | undefined>\n : T extends _ChannelPlatform\n ? ChannelPlatform\n : T extends _ChannelStatus\n ? ChannelStatus\n : T extends _ChannelAuthType\n ? ChannelAuthType\n : T extends _ConnectionHealth\n ? ConnectionHealth\n : T extends _AdapterType\n ? AdapterType\n : T extends _DeliveryPriority\n ? DeliveryPriority\n : T extends _DeliveryStatus\n ? DeliveryStatus\n : T extends _UserPresenceStatus\n ? UserPresenceStatus\n : T extends _PairingStatus\n ? PairingStatus\n : T extends _WebhookProcessingStatus\n ? WebhookProcessingStatus\n : T extends Array<infer U>\n ? Array<EnumToString<U>>\n : T extends object\n ? { [K in keyof T]: EnumToString<T[K]> }\n : T;\n\n// ── Input types (used by mutations) ─────────────────────────────────────────\n\nexport type CreateChannelInput = EnumToString<_CreateChannelInput>;\nexport type UpdateChannelInput = EnumToString<_UpdateChannelInput>;\nexport type CreateConnectionInput = EnumToString<_CreateConnectionInput>;\nexport type UpdateConnectionInput = EnumToString<_UpdateConnectionInput>;\nexport type ChannelFilterInput = EnumToString<_ChannelFilterInput>;\nexport type ChannelDeliveryContentInput = EnumToString<_ChannelDeliveryContentInput>;\nexport type ChannelDeliveryInput = EnumToString<_ChannelDeliveryInput>;\nexport type {\n ChannelPaginationInput,\n ChannelDeliveryOptionsInput,\n ChannelRetryPolicyInput,\n BulkChannelDeliveryInput,\n} from '../../generated/global-types';\n\n// ── Schema result types ─────────────────────────────────────────────────────\n\nexport type ChannelHealthCheck = EnumToString<_ChannelHealthCheck>;\nexport type ConnectionTestResult = EnumToString<_ConnectionTestResult>;\nexport type ChannelDeliveryResult = EnumToString<_ChannelDeliveryResult>;\nexport type ChannelDeliveryError = EnumToString<_ChannelDeliveryError>;\nexport type BulkChannelDeliveryResult = EnumToString<_BulkChannelDeliveryResult>;\n\n// ── Derived types from query results ──────────────────────────────────────\n// These represent the actual shape of data returned by our GraphQL operations,\n// not the full schema types (which include __typename, nested relations, etc.)\n\n/** Channel as returned by GetChannels query (list item) */\nexport type Channel = NonNullable<GetChannelsQuery['getChannels']['items'][number]>;\n\n/** Channel as returned by GetChannel query (full detail) */\nexport type ChannelDetail = NonNullable<GetChannelQuery['getChannel']>;\n\n/** ChannelConnection from the GetChannel query (full connection detail) */\nexport type ChannelConnection = NonNullable<ChannelDetail['connection']>;\n\n/** ChannelList response shape */\nexport type ChannelList = GetChannelsQuery['getChannels'];\n\n/** Message delivery attempt as returned by GetDeliveryAttempts query */\nexport type DeliveryAttempt = NonNullable<\n GetDeliveryAttemptsQuery['getChannelDeliveryAttempts']['items'][number]\n>;\n\n/** Webhook event as returned by GetWebhookEvents query */\nexport type WebhookEvent = NonNullable<\n GetWebhookEventsQuery['getChannelWebhookEvents']['items'][number]\n>;\n\n/** User platform connection as returned by GetUserPlatformConnections query */\nexport type UserPlatformConnection = NonNullable<\n GetUserPlatformConnectionsQuery['listUserPlatformConnectionsByChannel']['items'][number]\n>;\n\n/** Delivery statistics as returned by GetDeliveryStats query */\nexport type DeliveryStats = NonNullable<GetDeliveryStatsQuery['getDeliveryStats']>;\n\n// ============================================================================\n// HOOKS\n// ============================================================================\n\n/**\n * Hook for managing channels list\n */\nexport function useChannels(initialFilter?: ChannelFilterInput) {\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n const [channels, setChannels] = useState<Channel[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [hasMore, setHasMore] = useState(false);\n const [total, setTotal] = useState(0);\n const [filter, setFilter] = useState<ChannelFilterInput>(initialFilter ?? {});\n\n // Get Apollo client with auth token and organization context\n const getClient = useCallback(() => {\n return getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n }, [apiGatewayUrl, authToken, organizationId]);\n\n // Fetch channels\n const fetchChannels = useCallback(\n async (pagination?: { offset?: number; limit?: number }) => {\n setIsLoading(true);\n setError(null);\n\n try {\n const client = getClient();\n const result = await client.query<GetChannelsQuery>({\n query: GetChannelsDocument,\n variables: {\n filter,\n pagination: pagination ?? { offset: 0, limit: 50 },\n },\n fetchPolicy: 'network-only',\n });\n\n const data = result.data?.getChannels;\n if (data) {\n setChannels(data.items);\n setTotal(data.total);\n setHasMore(data.hasMore);\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch channels';\n report(err, 'Failed to fetch channels');\n setError(message);\n } finally {\n setIsLoading(false);\n }\n },\n [getClient, filter]\n );\n\n // Initial fetch — defer to microtask to avoid synchronous setState in effect\n useEffect(() => {\n void Promise.resolve().then(() => fetchChannels());\n }, [fetchChannels]);\n\n // Create channel\n const createChannel = useCallback(\n async (input: CreateChannelInput): Promise<Channel | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<CreateChannelMutation>({\n mutation: CreateChannelDocument,\n variables: { input },\n });\n const created = (result.data?.createChannel as Channel) ?? null;\n if (created) {\n emitShellEvent('channel.created', { id: created.id, platform: created.platform });\n }\n await fetchChannels();\n return created;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to create channel';\n report(err, 'Failed to create channel');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Update channel\n const updateChannel = useCallback(\n async (id: string, input: UpdateChannelInput): Promise<Channel | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<UpdateChannelMutation>({\n mutation: UpdateChannelDocument,\n variables: { id, input },\n });\n emitShellEvent('channel.updated', { id });\n await fetchChannels();\n return (result.data?.updateChannel as Channel) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update channel';\n report(err, 'Failed to update channel');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Delete channel\n const deleteChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeleteChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.deleted', { id });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to delete channel';\n report(err, 'Failed to delete channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Activate channel\n const activateChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: ActivateChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.updated', { id, action: 'activated' });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to activate channel';\n report(err, 'Failed to activate channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Deactivate channel\n const deactivateChannel = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeactivateChannelDocument,\n variables: { id },\n });\n emitShellEvent('channel.updated', { id, action: 'deactivated' });\n await fetchChannels();\n return true;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to deactivate channel';\n report(err, 'Failed to deactivate channel');\n setError(message);\n return false;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Run health check\n const runHealthCheck = useCallback(\n async (id: string): Promise<ChannelHealthCheck | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<RunChannelHealthCheckMutation>({\n mutation: RunChannelHealthCheckDocument,\n variables: { id },\n });\n await fetchChannels();\n return (result.data?.runChannelHealthCheck as ChannelHealthCheck) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to run health check';\n report(err, 'Failed to run health check');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannels]\n );\n\n // Update filter\n const updateFilter = useCallback((newFilter: Partial<ChannelFilterInput>) => {\n setFilter((prev) => ({ ...prev, ...newFilter }));\n }, []);\n\n // Reset filter\n const resetFilter = useCallback(() => {\n setFilter({});\n }, []);\n\n // Deliver notification via channel\n const deliverNotification = useCallback(\n async (\n input: ChannelDeliveryInput\n ): Promise<{ success: boolean; results?: ChannelDeliveryResult[]; error?: string }> => {\n try {\n const client = getClient();\n const result = await client.mutate<DeliverNotificationMutation>({\n mutation: DeliverNotificationDocument,\n variables: { input },\n });\n\n const data = result.data?.deliverNotification;\n if (data) {\n emitShellEvent('delivery.completed', {\n success: data.success ?? false,\n messageId: data.messageId,\n platform: data.platform,\n status: data.status,\n });\n return {\n success: data.success ?? false,\n results: [data as ChannelDeliveryResult],\n };\n }\n return { success: false, error: 'No data returned' };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to deliver notification';\n report(err, 'Failed to deliver notification');\n return { success: false, error: message };\n }\n },\n [getClient]\n );\n\n return {\n channels,\n isLoading,\n error,\n hasMore,\n total,\n filter,\n refetch: fetchChannels,\n createChannel,\n updateChannel,\n deleteChannel,\n activateChannel,\n deactivateChannel,\n runHealthCheck,\n updateFilter,\n resetFilter,\n deliverNotification,\n };\n}\n\n/**\n * Hook for managing a single channel\n */\nexport function useChannel(id: string | undefined) {\n const { authToken, apiGatewayUrl, organizationId } = useChannelContext();\n const [channel, setChannel] = useState<ChannelDetail | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Get Apollo client with auth token and organization context\n const getClient = useCallback(() => {\n return getGlobalChannelClient(apiGatewayUrl, authToken, organizationId);\n }, [apiGatewayUrl, authToken, organizationId]);\n\n // Fetch channel\n const fetchChannel = useCallback(async () => {\n if (!id) {\n setChannel(null);\n setIsLoading(false);\n return;\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n const client = getClient();\n const result = await client.query<GetChannelQuery>({\n query: GetChannelDocument,\n variables: { id },\n fetchPolicy: 'network-only',\n });\n\n setChannel(result.data?.getChannel ?? null);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch channel';\n report(err, 'Failed to fetch channel');\n setError(message);\n } finally {\n setIsLoading(false);\n }\n }, [id, getClient]);\n\n useEffect(() => {\n void Promise.resolve().then(() => fetchChannel());\n }, [fetchChannel]);\n\n // Update channel\n const updateChannel = useCallback(\n async (input: UpdateChannelInput): Promise<ChannelDetail | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<UpdateChannelMutation>({\n mutation: UpdateChannelDocument,\n variables: { id, input },\n });\n emitShellEvent('channel.updated', { id });\n await fetchChannel();\n return (result.data?.updateChannel as ChannelDetail) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update channel';\n report(err, 'Failed to update channel');\n setError(message);\n return null;\n }\n },\n [id, getClient, fetchChannel]\n );\n\n // Run health check\n const runHealthCheck = useCallback(async (): Promise<ChannelHealthCheck | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<RunChannelHealthCheckMutation>({\n mutation: RunChannelHealthCheckDocument,\n variables: { id },\n });\n await fetchChannel();\n return (result.data?.runChannelHealthCheck as ChannelHealthCheck) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to run health check';\n report(err, 'Failed to run health check');\n setError(message);\n return null;\n }\n }, [id, getClient, fetchChannel]);\n\n // Create connection\n const createConnection = useCallback(\n async (input: Omit<CreateConnectionInput, 'channelId'>): Promise<ChannelConnection | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.mutate<CreateConnectionMutation>({\n mutation: CreateConnectionDocument,\n variables: { input: { ...input, channelId: id } },\n });\n const created = (result.data?.createConnection as ChannelConnection) ?? null;\n if (created) {\n emitShellEvent('connection.created', { id: created.id, channelId: id });\n }\n await fetchChannel();\n return created;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to create connection';\n report(err, 'Failed to create connection');\n setError(message);\n return null;\n }\n },\n [id, getClient, fetchChannel]\n );\n\n // Update connection\n const updateConnection = useCallback(\n async (\n connectionId: string,\n input: UpdateConnectionInput\n ): Promise<ChannelConnection | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<UpdateConnectionMutation>({\n mutation: UpdateConnectionDocument,\n variables: { id: connectionId, input },\n });\n emitShellEvent('connection.updated', { id: connectionId });\n await fetchChannel();\n return (result.data?.updateConnection as ChannelConnection) ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to update connection';\n report(err, 'Failed to update connection');\n setError(message);\n return null;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Delete connection\n // Returns void on success; throws on failure (never returns false).\n const deleteConnection = useCallback(\n async (connectionId: string): Promise<void> => {\n try {\n const client = getClient();\n await client.mutate({\n mutation: DeleteConnectionDocument,\n variables: { id: connectionId },\n update(cache) {\n cache.evict({ id: `ChannelConnection:${connectionId}` });\n cache.gc();\n },\n });\n emitShellEvent('connection.deleted', { id: connectionId });\n await fetchChannel();\n } catch (err: unknown) {\n report(err, 'Failed to delete connection');\n throw err;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Test connection\n const testConnection = useCallback(\n async (connectionId: string): Promise<ConnectionTestResult | null> => {\n try {\n const client = getClient();\n const result = await client.mutate<TestConnectionHealthMutation>({\n mutation: TestConnectionHealthDocument,\n variables: { id: connectionId },\n });\n await fetchChannel();\n return (result.data?.testConnectionHealth as ConnectionTestResult) ?? null;\n } catch (err: unknown) {\n report(err, 'Failed to test connection');\n throw err;\n }\n },\n [getClient, fetchChannel]\n );\n\n // Send test message via channel using deliverNotification\n const sendTestNotification = useCallback(\n async (\n recipient: string,\n message: string,\n options?: { subject?: string; html?: string }\n ): Promise<ChannelDeliveryResult | null> => {\n if (!channel) return null;\n\n try {\n const client = getClient();\n\n // Build content based on platform\n const content: ChannelDeliveryContentInput = {\n text: message,\n };\n\n // Add platform-specific content\n if (channel.platform === 'EMAIL') {\n content.subject = options?.subject ?? 'Test Notification';\n if (options?.html) {\n content.html = options.html;\n }\n } else if (channel.platform === 'PUSH') {\n content.title = options?.subject ?? 'Test Notification';\n }\n\n const input: ChannelDeliveryInput = {\n platform: channel.platform,\n organizationId: channel.organizationId,\n recipient,\n content,\n connectionId: channel.connection?.id,\n };\n\n const result = await client.mutate<DeliverNotificationMutation>({\n mutation: DeliverNotificationDocument,\n variables: { input },\n });\n\n const out = (result.data?.deliverNotification as ChannelDeliveryResult) ?? null;\n if (out) {\n emitShellEvent('delivery.completed', {\n success: out.success ?? false,\n messageId: out.messageId,\n platform: out.platform,\n status: out.status,\n test: true,\n });\n }\n return out;\n } catch (err: unknown) {\n report(err, 'Failed to send test message');\n throw err;\n }\n },\n [channel, getClient]\n );\n\n // Fetch delivery attempts\n const fetchDeliveryAttempts = useCallback(\n async (limit: number = 50): Promise<DeliveryAttempt[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetDeliveryAttemptsQuery>({\n query: GetDeliveryAttemptsDocument,\n variables: { channelId: id, limit },\n fetchPolicy: 'network-only',\n });\n return result.data?.getChannelDeliveryAttempts?.items ?? [];\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch delivery attempts';\n report(err, 'Failed to fetch delivery attempts');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Fetch webhook events\n const fetchWebhookEvents = useCallback(\n async (limit: number = 50): Promise<WebhookEvent[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetWebhookEventsQuery>({\n query: GetWebhookEventsDocument,\n variables: { channelId: id, limit },\n fetchPolicy: 'network-only',\n });\n return result.data?.getChannelWebhookEvents?.items ?? [];\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch webhook events';\n report(err, 'Failed to fetch webhook events');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Fetch user platform connections\n const fetchUserPlatformConnections = useCallback(\n async (limit: number = 50): Promise<UserPlatformConnection[]> => {\n if (!id) return [];\n\n try {\n const client = getClient();\n const result = await client.query<GetUserPlatformConnectionsQuery>({\n query: GetUserPlatformConnectionsDocument,\n variables: { channelId: id, pagination: { limit } },\n fetchPolicy: 'network-only',\n });\n return result.data?.listUserPlatformConnectionsByChannel?.items ?? [];\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : 'Failed to fetch user platform connections';\n report(err, 'Failed to fetch user platform connections');\n setError(message);\n return [];\n }\n },\n [id, getClient]\n );\n\n // Retry a failed delivery\n const retryDelivery = useCallback(\n async (attemptId: string): Promise<{ success: boolean; error?: string }> => {\n try {\n const client = getClient();\n const result = await client.mutate<RetryDeliveryMutation>({\n mutation: RetryDeliveryDocument,\n variables: { attemptId },\n });\n const data = result.data?.retryDelivery;\n if (data?.success) {\n emitShellEvent('delivery.completed', {\n success: true,\n attemptId,\n messageId: data.messageId,\n retried: true,\n });\n // Refresh the delivery attempts list\n await fetchDeliveryAttempts();\n return { success: true };\n } else {\n return { success: false, error: data?.error || 'Retry failed' };\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to retry delivery';\n report(err, 'Failed to retry delivery');\n return { success: false, error: message };\n }\n },\n [getClient, fetchDeliveryAttempts]\n );\n\n // Fetch delivery statistics\n const fetchDeliveryStats = useCallback(\n async (startDate?: string, endDate?: string): Promise<DeliveryStats | null> => {\n if (!id) return null;\n\n try {\n const client = getClient();\n const result = await client.query<GetDeliveryStatsQuery>({\n query: GetDeliveryStatsDocument,\n variables: { channelId: id, startDate, endDate },\n fetchPolicy: 'network-only',\n });\n return result.data?.getDeliveryStats ?? null;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to fetch delivery stats';\n report(err, 'Failed to fetch delivery stats');\n setError(message);\n return null;\n }\n },\n [id, getClient]\n );\n\n return {\n channel,\n isLoading,\n error,\n refetch: fetchChannel,\n updateChannel,\n runHealthCheck,\n createConnection,\n updateConnection,\n deleteConnection,\n testConnection,\n sendTestNotification,\n fetchDeliveryAttempts,\n fetchWebhookEvents,\n fetchUserPlatformConnections,\n retryDelivery,\n fetchDeliveryStats,\n };\n}\n"],"mappings":"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"}
|
|
@@ -113,9 +113,9 @@ function x() {
|
|
|
113
113
|
})]
|
|
114
114
|
}),
|
|
115
115
|
M && /* @__PURE__ */ y("div", {
|
|
116
|
-
className: "p-4 rounded-lg bg-
|
|
117
|
-
children: [/* @__PURE__ */ v(u, { className: "w-5 h-5 text-
|
|
118
|
-
className: "text-sm text-
|
|
116
|
+
className: "p-4 rounded-lg bg-success-500/10 border border-success-500/20 flex items-center gap-3",
|
|
117
|
+
children: [/* @__PURE__ */ v(u, { className: "w-5 h-5 text-success-500 shrink-0" }), /* @__PURE__ */ v("p", {
|
|
118
|
+
className: "text-sm text-success-500",
|
|
119
119
|
children: C("channel.settingsSaved", "Settings saved successfully")
|
|
120
120
|
})]
|
|
121
121
|
}),
|
|
@@ -366,14 +366,14 @@ function x() {
|
|
|
366
366
|
})]
|
|
367
367
|
}),
|
|
368
368
|
/* @__PURE__ */ y("div", {
|
|
369
|
-
className: "p-4 rounded-lg border border-
|
|
369
|
+
className: "p-4 rounded-lg border border-warning-500/20 bg-warning-500/5",
|
|
370
370
|
children: [/* @__PURE__ */ y("div", {
|
|
371
371
|
className: "flex items-center justify-between mb-2",
|
|
372
372
|
children: [/* @__PURE__ */ v("p", {
|
|
373
373
|
className: "font-medium",
|
|
374
374
|
children: C("channel.failedDeliveryRetention", "Failed Delivery Retention")
|
|
375
375
|
}), /* @__PURE__ */ v("span", {
|
|
376
|
-
className: "text-sm text-
|
|
376
|
+
className: "text-sm text-warning-500",
|
|
377
377
|
children: "30 days"
|
|
378
378
|
})]
|
|
379
379
|
}), /* @__PURE__ */ v("p", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelSettingsPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelSettingsPage.tsx"],"sourcesContent":["/**\n * Channel Settings Page\n *\n * Global settings for the channels module.\n */\n\nimport { useState, useEffect, useRef } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Settings,\n Bell,\n Shield,\n Clock,\n Database,\n Save,\n Loader2,\n AlertCircle,\n CheckCircle2,\n} from 'lucide-react';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useChannelOrgSettings } from '../../../hooks/useChannelOrgSettings';\nimport type { ChannelOrgSettingsInput } from '../../../hooks/useChannelOrgSettings';\n\ninterface ChannelSettings {\n defaultPriority: number;\n rateLimitPerMinute: number;\n rateLimitPerHour: number;\n healthCheckIntervalMinutes: number;\n enableAuditLogging: boolean;\n enableWebhookVerification: boolean;\n sessionTimeoutMinutes: number;\n}\n\nconst DEFAULT_SETTINGS: ChannelSettings = {\n defaultPriority: 2,\n rateLimitPerMinute: 100,\n rateLimitPerHour: 5000,\n healthCheckIntervalMinutes: 5,\n enableAuditLogging: true,\n enableWebhookVerification: true,\n sessionTimeoutMinutes: 30,\n};\n\nexport function ChannelSettingsPage() {\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n settings: remoteSettings,\n isLoading: isLoadingSettings,\n error: settingsLoadError,\n saveSettings,\n } = useChannelOrgSettings();\n\n const [settings, setSettings] = useState<ChannelSettings>(DEFAULT_SETTINGS);\n const [isSaving, setIsSaving] = useState(false);\n const [saveSuccess, setSaveSuccess] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n // Populate / re-sync form from remote settings whenever they change.\n // This handles both the initial load and server-side normalisations returned\n // by saveSettings (e.g. clamped values), so the form always reflects the\n // true persisted state rather than potentially stale local values.\n // Defer setState to a microtask to avoid react-hooks/exhaustive-deps warnings.\n const loadedOrgIdRef = useRef<string | null>(null);\n useEffect(() => {\n if (!remoteSettings) return;\n // Re-hydrate whenever the org changes *or* the updatedAt timestamp changes,\n // so server-side coercion after a save is reflected immediately.\n const changeKey = `${remoteSettings.organizationId}:${remoteSettings.updatedAt}`;\n if (changeKey === loadedOrgIdRef.current) return;\n loadedOrgIdRef.current = changeKey;\n void Promise.resolve().then(() => {\n setSettings({\n defaultPriority: remoteSettings.defaultPriority,\n rateLimitPerMinute: remoteSettings.rateLimitPerMinute,\n rateLimitPerHour: remoteSettings.rateLimitPerHour,\n healthCheckIntervalMinutes: remoteSettings.healthCheckIntervalMinutes,\n enableAuditLogging: remoteSettings.enableAuditLogging,\n enableWebhookVerification: remoteSettings.enableWebhookVerification,\n sessionTimeoutMinutes: remoteSettings.sessionTimeoutMinutes,\n });\n });\n }, [remoteSettings]);\n\n const handleSave = async () => {\n // Guard: refuse to save when the initial load failed to prevent accidentally\n // overwriting existing org settings with client-side defaults (P1).\n if (settingsLoadError) {\n setSaveError(\n tr(\n 'channel.settingsLoadErrorSaveBlocked',\n 'Cannot save — settings failed to load. Please refresh and try again.'\n )\n );\n return;\n }\n\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n const input: ChannelOrgSettingsInput = {\n defaultPriority: settings.defaultPriority,\n rateLimitPerMinute: settings.rateLimitPerMinute,\n rateLimitPerHour: settings.rateLimitPerHour,\n healthCheckIntervalMinutes: settings.healthCheckIntervalMinutes,\n enableAuditLogging: settings.enableAuditLogging,\n enableWebhookVerification: settings.enableWebhookVerification,\n sessionTimeoutMinutes: settings.sessionTimeoutMinutes,\n };\n // saveSettings returns null when organizationId is absent (context not\n // ready); only show success when something was actually persisted (P2).\n const saved = await saveSettings(input);\n if (saved) {\n setSaveSuccess(true);\n setTimeout(() => setSaveSuccess(false), 3000);\n } else {\n setSaveError(\n tr(\n 'channel.settingsSaveNotPersisted',\n 'Settings could not be saved — organization context is unavailable.'\n )\n );\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to save settings';\n setSaveError(message);\n } finally {\n setIsSaving(false);\n }\n };\n\n const updateSetting = <K extends keyof ChannelSettings>(key: K, value: ChannelSettings[K]) => {\n setSettings((prev) => ({ ...prev, [key]: value }));\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <div className=\"min-h-screen bg-background\">\n {/* Header */}\n <div className=\"border-b bg-card\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n onClick={() => navigate('./list')}\n className=\"p-2 hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"w-5 h-5\" />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 bg-primary/10 rounded-lg\">\n <Settings className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.channelSettings', 'Channel Settings')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'channel.channelSettingsSubtitle',\n 'Configure global channel preferences'\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-2xl space-y-8\">\n {/* Loading state */}\n {isLoadingSettings && (\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n <span className=\"text-sm\">Loading settings…</span>\n </div>\n )}\n\n {/* Settings load error — shown persistently when the initial fetch failed */}\n {settingsLoadError && (\n <div className=\"p-4 rounded-lg bg-destructive/10 border border-destructive/20 flex items-center gap-3\">\n <AlertCircle className=\"w-5 h-5 text-destructive shrink-0\" />\n <p className=\"text-sm text-destructive\">\n {tr(\n 'channel.settingsLoadError',\n 'Failed to load settings — saving is disabled to prevent overwriting existing configuration.'\n )}\n </p>\n </div>\n )}\n\n {/* Alerts */}\n {saveSuccess && (\n <div className=\"p-4 rounded-lg bg-green-500/10 border border-green-500/20 flex items-center gap-3\">\n <CheckCircle2 className=\"w-5 h-5 text-green-500 shrink-0\" />\n <p className=\"text-sm text-green-500\">\n {tr('channel.settingsSaved', 'Settings saved successfully')}\n </p>\n </div>\n )}\n\n {saveError && (\n <div className=\"p-4 rounded-lg bg-destructive/10 border border-destructive/20 flex items-center gap-3\">\n <AlertCircle className=\"w-5 h-5 text-destructive shrink-0\" />\n <p className=\"text-sm text-destructive\">{saveError}</p>\n </div>\n )}\n\n {/* Delivery Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.deliverySettings', 'Delivery Settings')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.defaultPriority', 'Default Priority')}\n </label>\n <select\n value={settings.defaultPriority}\n onChange={(e) => updateSetting('defaultPriority', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={0}>0 - Lowest</option>\n <option value={1}>1 - Low</option>\n <option value={2}>2 - Normal</option>\n <option value={3}>3 - High</option>\n <option value={4}>4 - Highest</option>\n </select>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Priority used when creating new channels without explicit priority\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n </label>\n <input\n type=\"number\"\n value={settings.rateLimitPerMinute}\n onChange={(e) => updateSetting('rateLimitPerMinute', Number(e.target.value))}\n min={1}\n max={1000}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n </label>\n <input\n type=\"number\"\n value={settings.rateLimitPerHour}\n onChange={(e) => updateSetting('rateLimitPerHour', Number(e.target.value))}\n min={1}\n max={50000}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n </div>\n </div>\n </div>\n\n {/* Health Check Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.healthMonitoring', 'Health Monitoring')}\n </h2>\n </div>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n Health Check Interval (minutes)\n </label>\n <select\n value={settings.healthCheckIntervalMinutes}\n onChange={(e) =>\n updateSetting('healthCheckIntervalMinutes', Number(e.target.value))\n }\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={1}>1 minute</option>\n <option value={5}>5 minutes</option>\n <option value={10}>10 minutes</option>\n <option value={15}>15 minutes</option>\n <option value={30}>30 minutes</option>\n <option value={60}>1 hour</option>\n </select>\n <p className=\"text-xs text-muted-foreground mt-1\">\n How often to check connection health status\n </p>\n </div>\n </div>\n\n {/* Security Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">{tr('channel.security', 'Security')}</h2>\n </div>\n\n <div className=\"space-y-4\">\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-muted/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.webhookSignatureVerification', 'Webhook Signature Verification')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n Verify HMAC signatures on incoming webhooks\n </p>\n </div>\n <input\n type=\"checkbox\"\n checked={settings.enableWebhookVerification}\n onChange={(e) => updateSetting('enableWebhookVerification', e.target.checked)}\n className=\"w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-muted/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">{tr('channel.auditLogging', 'Audit Logging')}</p>\n <p className=\"text-sm text-muted-foreground\">\n Log all channel operations for audit trail\n </p>\n </div>\n <input\n type=\"checkbox\"\n checked={settings.enableAuditLogging}\n onChange={(e) => updateSetting('enableAuditLogging', e.target.checked)}\n className=\"w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.sessionTimeout', 'Session Timeout (minutes)')}\n </label>\n <select\n value={settings.sessionTimeoutMinutes}\n onChange={(e) => updateSetting('sessionTimeoutMinutes', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={15}>15 minutes</option>\n <option value={30}>30 minutes</option>\n <option value={60}>1 hour</option>\n <option value={120}>2 hours</option>\n <option value={240}>4 hours</option>\n </select>\n </div>\n </div>\n </div>\n\n {/* Data Management */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.dataManagement', 'Data Management')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.webhookEventRetention', 'Webhook Event Retention')}\n </p>\n <span className=\"text-sm text-muted-foreground\">7 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Incoming webhook events are automatically deleted after 7 days\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.deliveryAttemptRetention', 'Delivery Attempt Retention')}\n </p>\n <span className=\"text-sm text-muted-foreground\">7 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Successful delivery attempts are automatically deleted after 7 days\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border border-yellow-500/20 bg-yellow-500/5\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.failedDeliveryRetention', 'Failed Delivery Retention')}\n </p>\n <span className=\"text-sm text-yellow-500\">30 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Failed delivery attempts are kept for 30 days for debugging\n </p>\n </div>\n </div>\n </div>\n\n {/* Save Button */}\n <div className=\"flex justify-end pt-4\">\n <button\n onClick={handleSave}\n disabled={isSaving || isLoadingSettings || !!settingsLoadError}\n className=\"px-6 py-3 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center gap-2\"\n >\n {isSaving ? (\n <Loader2 className=\"w-5 h-5 animate-spin\" />\n ) : (\n <Save className=\"w-5 h-5\" />\n )}\n {tr('channel.saveSettings', 'Save Settings')}\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;AAoCA,IAAM,IAAoC;CACxC,iBAAiB;CACjB,oBAAoB;CACpB,kBAAkB;CAClB,4BAA4B;CAC5B,oBAAoB;CACpB,2BAA2B;CAC3B,uBAAuB;CACxB;AAED,SAAgB,IAAsB;CACpC,IAAM,IAAW,GAAa,EACxB,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAG9B,EACJ,UAAU,GACV,WAAW,GACX,OAAO,GACP,oBACE,GAAuB,EAErB,CAAC,GAAU,KAAe,EAA0B,EAAiB,EACrE,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAW,KAAgB,EAAwB,KAAK,EAOzD,IAAiB,EAAsB,KAAK;AAClD,SAAgB;AACd,MAAI,CAAC,EAAgB;EAGrB,IAAM,IAAY,GAAG,EAAe,eAAe,GAAG,EAAe;AACjE,QAAc,EAAe,YACjC,EAAe,UAAU,GACpB,QAAQ,SAAS,CAAC,WAAW;AAChC,KAAY;IACV,iBAAiB,EAAe;IAChC,oBAAoB,EAAe;IACnC,kBAAkB,EAAe;IACjC,4BAA4B,EAAe;IAC3C,oBAAoB,EAAe;IACnC,2BAA2B,EAAe;IAC1C,uBAAuB,EAAe;IACvC,CAAC;IACF;IACD,CAAC,EAAe,CAAC;CAEpB,IAAM,IAAa,YAAY;AAG7B,MAAI,GAAmB;AACrB,KACE,EACE,wCACA,uEACD,CACF;AACD;;AAKF,EAFA,EAAY,GAAK,EACjB,EAAa,KAAK,EAClB,EAAe,GAAM;AAErB,MAAI;AAaF,GADc,MAAM,EAXmB;IACrC,iBAAiB,EAAS;IAC1B,oBAAoB,EAAS;IAC7B,kBAAkB,EAAS;IAC3B,4BAA4B,EAAS;IACrC,oBAAoB,EAAS;IAC7B,2BAA2B,EAAS;IACpC,uBAAuB,EAAS;IACjC,CAGsC,IAErC,EAAe,GAAK,EACpB,iBAAiB,EAAe,GAAM,EAAE,IAAK,IAE7C,EACE,EACE,oCACA,qEACD,CACF;WAEI,GAAc;AAErB,KADgB,aAAe,QAAQ,EAAI,UAAU,0BAChC;YACb;AACR,KAAY,GAAM;;IAIhB,KAAkD,GAAQ,MAA8B;AAC5F,KAAa,OAAU;GAAE,GAAG;IAAO,IAAM;GAAO,EAAE;;AAGpD,QACE,kBAAC,GAAD;EAAiB,YAAY,EAAoB;YAC/C,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,eAAe,EAAS,SAAS;QACjC,WAAU;kBAEV,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;QAC1B,CAAA,EACT,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA;SACzC,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,2BAA2B,mBAAmB;SAC/C,CAAA,EACL,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,mCACA,uCACD;SACC,CAAA,CACA,EAAA,CAAA,CACF;UACF;;MACF,CAAA;KACF,CAAA;IACF,CAAA,EAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEG,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAC5C,kBAAC,QAAD;OAAM,WAAU;iBAAU;OAAwB,CAAA,CAC9C;;KAIP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,qCAAsC,CAAA,EAC7D,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,6BACA,8FACD;OACC,CAAA,CACA;;KAIP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAc,WAAU,mCAAoC,CAAA,EAC5D,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,yBAAyB,8BAA8B;OACzD,CAAA,CACA;;KAGP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,qCAAsC,CAAA,EAC7D,kBAAC,KAAD;OAAG,WAAU;iBAA4B;OAAc,CAAA,CACnD;;KAIR,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,wBAAyB,CAAA,EACzC,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,4BAA4B,oBAAoB;QACjD,CAAA,CACD;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,2BAA2B,mBAAmB;SAC5C,CAAA;QACR,kBAAC,UAAD;SACE,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,mBAAmB,OAAO,EAAE,OAAO,MAAM,CAAC;SACzE,WAAU;mBAHZ;UAKE,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAmB,CAAA;UACrC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAgB,CAAA;UAClC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAmB,CAAA;UACrC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAiB,CAAA;UACnC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAoB,CAAA;UAC/B;;QACT,kBAAC,KAAD;SAAG,WAAU;mBAAqC;SAE9C,CAAA;QACA,EAAA,CAAA,EAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,2BAA2B,0BAA0B;SACnD,CAAA,EACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,sBAAsB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC5E,KAAK;SACL,KAAK;SACL,WAAU;SACV,CAAA,CACE,EAAA,CAAA,EAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,4BAA4B,wBAAwB;SAClD,CAAA,EACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,oBAAoB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC1E,KAAK;SACL,KAAK;SACL,WAAU;SACV,CAAA,CACE,EAAA,CAAA,CACF;UACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,wBAAyB,CAAA,EAC1C,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,4BAA4B,oBAAoB;QACjD,CAAA,CACD;UAEN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAiC;QAE1C,CAAA;OACR,kBAAC,UAAD;QACE,OAAO,EAAS;QAChB,WAAW,MACT,EAAc,8BAA8B,OAAO,EAAE,OAAO,MAAM,CAAC;QAErE,WAAU;kBALZ;SAOE,kBAAC,UAAD;UAAQ,OAAO;oBAAG;UAAiB,CAAA;SACnC,kBAAC,UAAD;UAAQ,OAAO;oBAAG;UAAkB,CAAA;SACpC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAe,CAAA;SAC3B;;OACT,kBAAC,KAAD;QAAG,WAAU;kBAAqC;QAE9C,CAAA;OACA,EAAA,CAAA,CACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wBAAyB,CAAA,EAC3C,kBAAC,MAAD;QAAI,WAAU;kBAAyB,EAAG,oBAAoB,WAAW;QAAM,CAAA,CAC3E;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAG,wCAAwC,iCAAiC;UAC3E,CAAA,EACJ,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,SAAD;UACE,MAAK;UACL,SAAS,EAAS;UAClB,WAAW,MAAM,EAAc,6BAA6B,EAAE,OAAO,QAAQ;UAC7E,WAAU;UACV,CAAA,CACI;;QAER,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAe,EAAG,wBAAwB,gBAAgB;UAAK,CAAA,EAC5E,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,SAAD;UACE,MAAK;UACL,SAAS,EAAS;UAClB,WAAW,MAAM,EAAc,sBAAsB,EAAE,OAAO,QAAQ;UACtE,WAAU;UACV,CAAA,CACI;;QAER,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,0BAA0B,4BAA4B;SACpD,CAAA,EACR,kBAAC,UAAD;SACE,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,yBAAyB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC/E,WAAU;mBAHZ;UAKE,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAmB,CAAA;UACtC,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAmB,CAAA;UACtC,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAe,CAAA;UAClC,kBAAC,UAAD;WAAQ,OAAO;qBAAK;WAAgB,CAAA;UACpC,kBAAC,UAAD;WAAQ,OAAO;qBAAK;WAAgB,CAAA;UAC7B;WACL,EAAA,CAAA;QACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA,EAC7C,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,0BAA0B,kBAAkB;QAC7C,CAAA,CACD;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,iCAAiC,0BAA0B;WAC7D,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAAgC;WAAa,CAAA,CACzD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,oCAAoC,6BAA6B;WACnE,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAAgC;WAAa,CAAA,CACzD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,mCAAmC,4BAA4B;WACjE,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAA0B;WAAc,CAAA,CACpD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,SAAS;OACT,UAAU,KAAY,KAAqB,CAAC,CAAC;OAC7C,WAAU;iBAHZ,CAKG,IACC,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAE7B,EAAG,wBAAwB,gBAAgB,CACrC;;MACL,CAAA;KACF;MACF;;EACU,CAAA"}
|
|
1
|
+
{"version":3,"file":"ChannelSettingsPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelSettingsPage.tsx"],"sourcesContent":["/**\n * Channel Settings Page\n *\n * Global settings for the channels module.\n */\n\nimport { useState, useEffect, useRef } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Settings,\n Bell,\n Shield,\n Clock,\n Database,\n Save,\n Loader2,\n AlertCircle,\n CheckCircle2,\n} from 'lucide-react';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useChannelOrgSettings } from '../../../hooks/useChannelOrgSettings';\nimport type { ChannelOrgSettingsInput } from '../../../hooks/useChannelOrgSettings';\n\ninterface ChannelSettings {\n defaultPriority: number;\n rateLimitPerMinute: number;\n rateLimitPerHour: number;\n healthCheckIntervalMinutes: number;\n enableAuditLogging: boolean;\n enableWebhookVerification: boolean;\n sessionTimeoutMinutes: number;\n}\n\nconst DEFAULT_SETTINGS: ChannelSettings = {\n defaultPriority: 2,\n rateLimitPerMinute: 100,\n rateLimitPerHour: 5000,\n healthCheckIntervalMinutes: 5,\n enableAuditLogging: true,\n enableWebhookVerification: true,\n sessionTimeoutMinutes: 30,\n};\n\nexport function ChannelSettingsPage() {\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n settings: remoteSettings,\n isLoading: isLoadingSettings,\n error: settingsLoadError,\n saveSettings,\n } = useChannelOrgSettings();\n\n const [settings, setSettings] = useState<ChannelSettings>(DEFAULT_SETTINGS);\n const [isSaving, setIsSaving] = useState(false);\n const [saveSuccess, setSaveSuccess] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n // Populate / re-sync form from remote settings whenever they change.\n // This handles both the initial load and server-side normalisations returned\n // by saveSettings (e.g. clamped values), so the form always reflects the\n // true persisted state rather than potentially stale local values.\n // Defer setState to a microtask to avoid react-hooks/exhaustive-deps warnings.\n const loadedOrgIdRef = useRef<string | null>(null);\n useEffect(() => {\n if (!remoteSettings) return;\n // Re-hydrate whenever the org changes *or* the updatedAt timestamp changes,\n // so server-side coercion after a save is reflected immediately.\n const changeKey = `${remoteSettings.organizationId}:${remoteSettings.updatedAt}`;\n if (changeKey === loadedOrgIdRef.current) return;\n loadedOrgIdRef.current = changeKey;\n void Promise.resolve().then(() => {\n setSettings({\n defaultPriority: remoteSettings.defaultPriority,\n rateLimitPerMinute: remoteSettings.rateLimitPerMinute,\n rateLimitPerHour: remoteSettings.rateLimitPerHour,\n healthCheckIntervalMinutes: remoteSettings.healthCheckIntervalMinutes,\n enableAuditLogging: remoteSettings.enableAuditLogging,\n enableWebhookVerification: remoteSettings.enableWebhookVerification,\n sessionTimeoutMinutes: remoteSettings.sessionTimeoutMinutes,\n });\n });\n }, [remoteSettings]);\n\n const handleSave = async () => {\n // Guard: refuse to save when the initial load failed to prevent accidentally\n // overwriting existing org settings with client-side defaults (P1).\n if (settingsLoadError) {\n setSaveError(\n tr(\n 'channel.settingsLoadErrorSaveBlocked',\n 'Cannot save — settings failed to load. Please refresh and try again.'\n )\n );\n return;\n }\n\n setIsSaving(true);\n setSaveError(null);\n setSaveSuccess(false);\n\n try {\n const input: ChannelOrgSettingsInput = {\n defaultPriority: settings.defaultPriority,\n rateLimitPerMinute: settings.rateLimitPerMinute,\n rateLimitPerHour: settings.rateLimitPerHour,\n healthCheckIntervalMinutes: settings.healthCheckIntervalMinutes,\n enableAuditLogging: settings.enableAuditLogging,\n enableWebhookVerification: settings.enableWebhookVerification,\n sessionTimeoutMinutes: settings.sessionTimeoutMinutes,\n };\n // saveSettings returns null when organizationId is absent (context not\n // ready); only show success when something was actually persisted (P2).\n const saved = await saveSettings(input);\n if (saved) {\n setSaveSuccess(true);\n setTimeout(() => setSaveSuccess(false), 3000);\n } else {\n setSaveError(\n tr(\n 'channel.settingsSaveNotPersisted',\n 'Settings could not be saved — organization context is unavailable.'\n )\n );\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : 'Failed to save settings';\n setSaveError(message);\n } finally {\n setIsSaving(false);\n }\n };\n\n const updateSetting = <K extends keyof ChannelSettings>(key: K, value: ChannelSettings[K]) => {\n setSettings((prev) => ({ ...prev, [key]: value }));\n };\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_UPDATE}>\n <div className=\"min-h-screen bg-background\">\n {/* Header */}\n <div className=\"border-b bg-card\">\n <div className=\"container mx-auto px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <button\n onClick={() => navigate('./list')}\n className=\"p-2 hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft className=\"w-5 h-5\" />\n </button>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-2 bg-primary/10 rounded-lg\">\n <Settings className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <h1 className=\"text-2xl font-bold\">\n {tr('channel.channelSettings', 'Channel Settings')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'channel.channelSettingsSubtitle',\n 'Configure global channel preferences'\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container mx-auto px-6 py-8 max-w-2xl space-y-8\">\n {/* Loading state */}\n {isLoadingSettings && (\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n <span className=\"text-sm\">Loading settings…</span>\n </div>\n )}\n\n {/* Settings load error — shown persistently when the initial fetch failed */}\n {settingsLoadError && (\n <div className=\"p-4 rounded-lg bg-destructive/10 border border-destructive/20 flex items-center gap-3\">\n <AlertCircle className=\"w-5 h-5 text-destructive shrink-0\" />\n <p className=\"text-sm text-destructive\">\n {tr(\n 'channel.settingsLoadError',\n 'Failed to load settings — saving is disabled to prevent overwriting existing configuration.'\n )}\n </p>\n </div>\n )}\n\n {/* Alerts */}\n {saveSuccess && (\n <div className=\"p-4 rounded-lg bg-success-500/10 border border-success-500/20 flex items-center gap-3\">\n <CheckCircle2 className=\"w-5 h-5 text-success-500 shrink-0\" />\n <p className=\"text-sm text-success-500\">\n {tr('channel.settingsSaved', 'Settings saved successfully')}\n </p>\n </div>\n )}\n\n {saveError && (\n <div className=\"p-4 rounded-lg bg-destructive/10 border border-destructive/20 flex items-center gap-3\">\n <AlertCircle className=\"w-5 h-5 text-destructive shrink-0\" />\n <p className=\"text-sm text-destructive\">{saveError}</p>\n </div>\n )}\n\n {/* Delivery Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.deliverySettings', 'Delivery Settings')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.defaultPriority', 'Default Priority')}\n </label>\n <select\n value={settings.defaultPriority}\n onChange={(e) => updateSetting('defaultPriority', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={0}>0 - Lowest</option>\n <option value={1}>1 - Low</option>\n <option value={2}>2 - Normal</option>\n <option value={3}>3 - High</option>\n <option value={4}>4 - Highest</option>\n </select>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Priority used when creating new channels without explicit priority\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.rateLimitPerMin', 'Rate Limit (per minute)')}\n </label>\n <input\n type=\"number\"\n value={settings.rateLimitPerMinute}\n onChange={(e) => updateSetting('rateLimitPerMinute', Number(e.target.value))}\n min={1}\n max={1000}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.rateLimitPerHour', 'Rate Limit (per hour)')}\n </label>\n <input\n type=\"number\"\n value={settings.rateLimitPerHour}\n onChange={(e) => updateSetting('rateLimitPerHour', Number(e.target.value))}\n min={1}\n max={50000}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n </div>\n </div>\n </div>\n\n {/* Health Check Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Clock className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.healthMonitoring', 'Health Monitoring')}\n </h2>\n </div>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n Health Check Interval (minutes)\n </label>\n <select\n value={settings.healthCheckIntervalMinutes}\n onChange={(e) =>\n updateSetting('healthCheckIntervalMinutes', Number(e.target.value))\n }\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={1}>1 minute</option>\n <option value={5}>5 minutes</option>\n <option value={10}>10 minutes</option>\n <option value={15}>15 minutes</option>\n <option value={30}>30 minutes</option>\n <option value={60}>1 hour</option>\n </select>\n <p className=\"text-xs text-muted-foreground mt-1\">\n How often to check connection health status\n </p>\n </div>\n </div>\n\n {/* Security Settings */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">{tr('channel.security', 'Security')}</h2>\n </div>\n\n <div className=\"space-y-4\">\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-muted/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">\n {tr('channel.webhookSignatureVerification', 'Webhook Signature Verification')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n Verify HMAC signatures on incoming webhooks\n </p>\n </div>\n <input\n type=\"checkbox\"\n checked={settings.enableWebhookVerification}\n onChange={(e) => updateSetting('enableWebhookVerification', e.target.checked)}\n className=\"w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <label className=\"flex items-center justify-between p-4 rounded-lg border hover:bg-muted/50 cursor-pointer\">\n <div>\n <p className=\"font-medium\">{tr('channel.auditLogging', 'Audit Logging')}</p>\n <p className=\"text-sm text-muted-foreground\">\n Log all channel operations for audit trail\n </p>\n </div>\n <input\n type=\"checkbox\"\n checked={settings.enableAuditLogging}\n onChange={(e) => updateSetting('enableAuditLogging', e.target.checked)}\n className=\"w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary\"\n />\n </label>\n\n <div>\n <label className=\"text-sm font-medium block mb-1\">\n {tr('channel.sessionTimeout', 'Session Timeout (minutes)')}\n </label>\n <select\n value={settings.sessionTimeoutMinutes}\n onChange={(e) => updateSetting('sessionTimeoutMinutes', Number(e.target.value))}\n className=\"w-full px-4 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value={15}>15 minutes</option>\n <option value={30}>30 minutes</option>\n <option value={60}>1 hour</option>\n <option value={120}>2 hours</option>\n <option value={240}>4 hours</option>\n </select>\n </div>\n </div>\n </div>\n\n {/* Data Management */}\n <div className=\"border rounded-lg p-6 space-y-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"w-5 h-5 text-primary\" />\n <h2 className=\"text-lg font-semibold\">\n {tr('channel.dataManagement', 'Data Management')}\n </h2>\n </div>\n\n <div className=\"space-y-4\">\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.webhookEventRetention', 'Webhook Event Retention')}\n </p>\n <span className=\"text-sm text-muted-foreground\">7 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Incoming webhook events are automatically deleted after 7 days\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.deliveryAttemptRetention', 'Delivery Attempt Retention')}\n </p>\n <span className=\"text-sm text-muted-foreground\">7 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Successful delivery attempts are automatically deleted after 7 days\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg border border-warning-500/20 bg-warning-500/5\">\n <div className=\"flex items-center justify-between mb-2\">\n <p className=\"font-medium\">\n {tr('channel.failedDeliveryRetention', 'Failed Delivery Retention')}\n </p>\n <span className=\"text-sm text-warning-500\">30 days</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Failed delivery attempts are kept for 30 days for debugging\n </p>\n </div>\n </div>\n </div>\n\n {/* Save Button */}\n <div className=\"flex justify-end pt-4\">\n <button\n onClick={handleSave}\n disabled={isSaving || isLoadingSettings || !!settingsLoadError}\n className=\"px-6 py-3 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center gap-2\"\n >\n {isSaving ? (\n <Loader2 className=\"w-5 h-5 animate-spin\" />\n ) : (\n <Save className=\"w-5 h-5\" />\n )}\n {tr('channel.saveSettings', 'Save Settings')}\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;AAoCA,IAAM,IAAoC;CACxC,iBAAiB;CACjB,oBAAoB;CACpB,kBAAkB;CAClB,4BAA4B;CAC5B,oBAAoB;CACpB,2BAA2B;CAC3B,uBAAuB;CACxB;AAED,SAAgB,IAAsB;CACpC,IAAM,IAAW,GAAa,EACxB,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAG9B,EACJ,UAAU,GACV,WAAW,GACX,OAAO,GACP,oBACE,GAAuB,EAErB,CAAC,GAAU,KAAe,EAA0B,EAAiB,EACrE,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAW,KAAgB,EAAwB,KAAK,EAOzD,IAAiB,EAAsB,KAAK;AAClD,SAAgB;AACd,MAAI,CAAC,EAAgB;EAGrB,IAAM,IAAY,GAAG,EAAe,eAAe,GAAG,EAAe;AACjE,QAAc,EAAe,YACjC,EAAe,UAAU,GACpB,QAAQ,SAAS,CAAC,WAAW;AAChC,KAAY;IACV,iBAAiB,EAAe;IAChC,oBAAoB,EAAe;IACnC,kBAAkB,EAAe;IACjC,4BAA4B,EAAe;IAC3C,oBAAoB,EAAe;IACnC,2BAA2B,EAAe;IAC1C,uBAAuB,EAAe;IACvC,CAAC;IACF;IACD,CAAC,EAAe,CAAC;CAEpB,IAAM,IAAa,YAAY;AAG7B,MAAI,GAAmB;AACrB,KACE,EACE,wCACA,uEACD,CACF;AACD;;AAKF,EAFA,EAAY,GAAK,EACjB,EAAa,KAAK,EAClB,EAAe,GAAM;AAErB,MAAI;AAaF,GADc,MAAM,EAXmB;IACrC,iBAAiB,EAAS;IAC1B,oBAAoB,EAAS;IAC7B,kBAAkB,EAAS;IAC3B,4BAA4B,EAAS;IACrC,oBAAoB,EAAS;IAC7B,2BAA2B,EAAS;IACpC,uBAAuB,EAAS;IACjC,CAGsC,IAErC,EAAe,GAAK,EACpB,iBAAiB,EAAe,GAAM,EAAE,IAAK,IAE7C,EACE,EACE,oCACA,qEACD,CACF;WAEI,GAAc;AAErB,KADgB,aAAe,QAAQ,EAAI,UAAU,0BAChC;YACb;AACR,KAAY,GAAM;;IAIhB,KAAkD,GAAQ,MAA8B;AAC5F,KAAa,OAAU;GAAE,GAAG;IAAO,IAAM;GAAO,EAAE;;AAGpD,QACE,kBAAC,GAAD;EAAiB,YAAY,EAAoB;YAC/C,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,eAAe,EAAS,SAAS;QACjC,WAAU;kBAEV,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;QAC1B,CAAA,EACT,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA;SACzC,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,2BAA2B,mBAAmB;SAC/C,CAAA,EACL,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,mCACA,uCACD;SACC,CAAA,CACA,EAAA,CAAA,CACF;UACF;;MACF,CAAA;KACF,CAAA;IACF,CAAA,EAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEG,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAC5C,kBAAC,QAAD;OAAM,WAAU;iBAAU;OAAwB,CAAA,CAC9C;;KAIP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,qCAAsC,CAAA,EAC7D,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,6BACA,8FACD;OACC,CAAA,CACA;;KAIP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAc,WAAU,qCAAsC,CAAA,EAC9D,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,yBAAyB,8BAA8B;OACzD,CAAA,CACA;;KAGP,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,qCAAsC,CAAA,EAC7D,kBAAC,KAAD;OAAG,WAAU;iBAA4B;OAAc,CAAA,CACnD;;KAIR,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,wBAAyB,CAAA,EACzC,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,4BAA4B,oBAAoB;QACjD,CAAA,CACD;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,2BAA2B,mBAAmB;SAC5C,CAAA;QACR,kBAAC,UAAD;SACE,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,mBAAmB,OAAO,EAAE,OAAO,MAAM,CAAC;SACzE,WAAU;mBAHZ;UAKE,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAmB,CAAA;UACrC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAgB,CAAA;UAClC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAmB,CAAA;UACrC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAiB,CAAA;UACnC,kBAAC,UAAD;WAAQ,OAAO;qBAAG;WAAoB,CAAA;UAC/B;;QACT,kBAAC,KAAD;SAAG,WAAU;mBAAqC;SAE9C,CAAA;QACA,EAAA,CAAA,EAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,2BAA2B,0BAA0B;SACnD,CAAA,EACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,sBAAsB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC5E,KAAK;SACL,KAAK;SACL,WAAU;SACV,CAAA,CACE,EAAA,CAAA,EAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,4BAA4B,wBAAwB;SAClD,CAAA,EACR,kBAAC,SAAD;SACE,MAAK;SACL,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,oBAAoB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC1E,KAAK;SACL,KAAK;SACL,WAAU;SACV,CAAA,CACE,EAAA,CAAA,CACF;UACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,wBAAyB,CAAA,EAC1C,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,4BAA4B,oBAAoB;QACjD,CAAA,CACD;UAEN,kBAAC,OAAD,EAAA,UAAA;OACE,kBAAC,SAAD;QAAO,WAAU;kBAAiC;QAE1C,CAAA;OACR,kBAAC,UAAD;QACE,OAAO,EAAS;QAChB,WAAW,MACT,EAAc,8BAA8B,OAAO,EAAE,OAAO,MAAM,CAAC;QAErE,WAAU;kBALZ;SAOE,kBAAC,UAAD;UAAQ,OAAO;oBAAG;UAAiB,CAAA;SACnC,kBAAC,UAAD;UAAQ,OAAO;oBAAG;UAAkB,CAAA;SACpC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAmB,CAAA;SACtC,kBAAC,UAAD;UAAQ,OAAO;oBAAI;UAAe,CAAA;SAC3B;;OACT,kBAAC,KAAD;QAAG,WAAU;kBAAqC;QAE9C,CAAA;OACA,EAAA,CAAA,CACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wBAAyB,CAAA,EAC3C,kBAAC,MAAD;QAAI,WAAU;kBAAyB,EAAG,oBAAoB,WAAW;QAAM,CAAA,CAC3E;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAG,wCAAwC,iCAAiC;UAC3E,CAAA,EACJ,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,SAAD;UACE,MAAK;UACL,SAAS,EAAS;UAClB,WAAW,MAAM,EAAc,6BAA6B,EAAE,OAAO,QAAQ;UAC7E,WAAU;UACV,CAAA,CACI;;QAER,kBAAC,SAAD;SAAO,WAAU;mBAAjB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAe,EAAG,wBAAwB,gBAAgB;UAAK,CAAA,EAC5E,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,SAAD;UACE,MAAK;UACL,SAAS,EAAS;UAClB,WAAW,MAAM,EAAc,sBAAsB,EAAE,OAAO,QAAQ;UACtE,WAAU;UACV,CAAA,CACI;;QAER,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;SAAO,WAAU;mBACd,EAAG,0BAA0B,4BAA4B;SACpD,CAAA,EACR,kBAAC,UAAD;SACE,OAAO,EAAS;SAChB,WAAW,MAAM,EAAc,yBAAyB,OAAO,EAAE,OAAO,MAAM,CAAC;SAC/E,WAAU;mBAHZ;UAKE,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAmB,CAAA;UACtC,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAmB,CAAA;UACtC,kBAAC,UAAD;WAAQ,OAAO;qBAAI;WAAe,CAAA;UAClC,kBAAC,UAAD;WAAQ,OAAO;qBAAK;WAAgB,CAAA;UACpC,kBAAC,UAAD;WAAQ,OAAO;qBAAK;WAAgB,CAAA;UAC7B;WACL,EAAA,CAAA;QACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA,EAC7C,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAG,0BAA0B,kBAAkB;QAC7C,CAAA,CACD;UAEN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,iCAAiC,0BAA0B;WAC7D,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAAgC;WAAa,CAAA,CACzD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,oCAAoC,6BAA6B;WACnE,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAAgC;WAAa,CAAA,CACzD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QAEN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAG,mCAAmC,4BAA4B;WACjE,CAAA,EACJ,kBAAC,QAAD;WAAM,WAAU;qBAA2B;WAAc,CAAA,CACrD;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAgC;UAEzC,CAAA,CACA;;QACF;SACF;;KAGN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,SAAS;OACT,UAAU,KAAY,KAAqB,CAAC,CAAC;OAC7C,WAAU;iBAHZ,CAKG,IACC,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAE7B,EAAG,wBAAwB,gBAAgB,CACrC;;MACL,CAAA;KACF;MACF;;EACU,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelUsersPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA8CH,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"ChannelUsersPage.d.ts","sourceRoot":"","sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA8CH,wBAAgB,gBAAgB,4CA0R/B"}
|
|
@@ -102,8 +102,12 @@ function D() {
|
|
|
102
102
|
className: "flex items-center gap-3",
|
|
103
103
|
children: [/* @__PURE__ */ C("button", {
|
|
104
104
|
onClick: () => m(`/channels/${p}`),
|
|
105
|
+
"aria-label": O("channel.backToChannel", "Back to channel"),
|
|
105
106
|
className: "p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors",
|
|
106
|
-
children: /* @__PURE__ */ C(u, {
|
|
107
|
+
children: /* @__PURE__ */ C(u, {
|
|
108
|
+
size: 20,
|
|
109
|
+
"aria-hidden": "true"
|
|
110
|
+
})
|
|
107
111
|
}), /* @__PURE__ */ w("div", { children: [/* @__PURE__ */ C("h1", {
|
|
108
112
|
className: "text-xl sm:text-2xl font-bold text-foreground",
|
|
109
113
|
children: O("channel.channelUsers", "Channel Users")
|
|
@@ -280,12 +284,26 @@ function D() {
|
|
|
280
284
|
}),
|
|
281
285
|
/* @__PURE__ */ C("td", {
|
|
282
286
|
className: "px-4 py-3",
|
|
283
|
-
children: e.notificationsEnabled ? /* @__PURE__ */
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
children: e.notificationsEnabled ? /* @__PURE__ */ w("span", {
|
|
288
|
+
className: "inline-flex items-center gap-1",
|
|
289
|
+
children: [/* @__PURE__ */ C(d, {
|
|
290
|
+
size: 16,
|
|
291
|
+
className: "text-success-500",
|
|
292
|
+
"aria-hidden": "true"
|
|
293
|
+
}), /* @__PURE__ */ C("span", {
|
|
294
|
+
className: "sr-only",
|
|
295
|
+
children: O("channel.enabled", "Enabled")
|
|
296
|
+
})]
|
|
297
|
+
}) : /* @__PURE__ */ w("span", {
|
|
298
|
+
className: "inline-flex items-center gap-1",
|
|
299
|
+
children: [/* @__PURE__ */ C(f, {
|
|
300
|
+
size: 16,
|
|
301
|
+
className: "text-muted-foreground",
|
|
302
|
+
"aria-hidden": "true"
|
|
303
|
+
}), /* @__PURE__ */ C("span", {
|
|
304
|
+
className: "sr-only",
|
|
305
|
+
children: O("channel.disabled", "Disabled")
|
|
306
|
+
})]
|
|
289
307
|
})
|
|
290
308
|
}),
|
|
291
309
|
/* @__PURE__ */ C("td", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n} from 'lucide-react';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nconst PRESENCE_CONFIG: Record<UserPresenceStatus, { label: string; color: string }> = {\n ONLINE: { label: 'Online', color: 'text-success-500' },\n OFFLINE: { label: 'Offline', color: 'text-muted-foreground' },\n AWAY: { label: 'Away', color: 'text-warning-500' },\n DND: { label: 'Do Not Disturb', color: 'text-error-500' },\n};\n\nconst PAIRING_CONFIG: Record<\n PairingStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: { label: 'Pending', color: 'text-warning-500 bg-warning-100', icon: Clock },\n APPROVED: { label: 'Approved', color: 'text-success-500 bg-success-100', icon: CheckCircle2 },\n EXPIRED: { label: 'Expired', color: 'text-muted-foreground bg-muted', icon: XCircle },\n};\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(async () => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const data = await fetchUserPlatformConnections(limit);\n setUsers(data);\n } finally {\n setIsLoadingUsers(false);\n }\n }, [channelId, fetchUserPlatformConnections, limit]);\n\n useEffect(() => {\n void Promise.resolve().then(() => loadUsers());\n }, [loadUsers]);\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n // Loading state\n if (isLoading || isLoadingUsers) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-error-100 border border-error-500 rounded-lg\">\n <AlertCircle size={20} className=\"text-error-500\" />\n <div>\n <p className=\"text-sm font-medium text-error-700\">{error || 'Channel not found'}</p>\n </div>\n <button\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-error-700 hover:bg-error-200 rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-foreground\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {channel.name} - User connections\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-foreground\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <button\n onClick={loadUsers}\n disabled={isLoadingUsers}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors disabled:opacity-50\"\n >\n <RefreshCw size={16} className={isLoadingUsers ? 'animate-spin' : ''} />\n {tr('channel.refresh', 'Refresh')}\n </button>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_ADMIN}>\n <button\n onClick={() => void exportUsers()}\n disabled={isExportingUsers}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-fg border border-border hover:bg-bg-elevated rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n title={tr('channel.exportCSV', 'Export to CSV')}\n >\n {isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Download size={16} />\n )}\n {tr('channel.export', 'Export')}\n </button>\n </PermissionGuard>\n </div>\n </div>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-error-100 border border-error-500 rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-error-500\" />\n <p className=\"text-sm text-error-700 flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Info Banner */}\n <div className=\"p-4 bg-surface border border-border rounded-lg\">\n <p className=\"text-sm text-muted-foreground\">\n Users connected to this channel through OAuth or pairing. Manage notification\n preferences and DM authorization here.\n </p>\n </div>\n\n {/* Users List */}\n <div className=\"bg-surface border border-border rounded-lg overflow-hidden\">\n <div className=\"overflow-x-auto min-w-full\">\n <table className=\"w-full min-w-[800px]\">\n <thead className=\"bg-muted/50 border-b border-border\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.user', 'User')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.platformId', 'Platform ID')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.source', 'Source')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.status', 'Status')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.pairing', 'Pairing')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.notifications', 'Notifications')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.dmAllowed', 'DM Allowed')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {users.length === 0 ? (\n <tr>\n <td colSpan={7} className=\"px-4 py-8 text-center text-muted-foreground\">\n {tr('channel.noUsersConnected', 'No users connected to this channel')}\n </td>\n </tr>\n ) : (\n users.map((user) => {\n const presenceConfig = getPresenceConfig(\n user.presenceStatus as UserPresenceStatus\n );\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return (\n <tr key={user.id} className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {user.externalDisplayName || 'Unknown'}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-muted-foreground\">\n {user.externalUsername}\n </p>\n )}\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm font-mono text-muted-foreground\">\n {user.externalUserId}\n </td>\n <td className=\"px-4 py-3\">\n <span className=\"text-xs text-muted-foreground bg-muted px-2 py-1 rounded\">\n {user.source}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>\n {presenceConfig.label}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3\">\n {user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {pairingConfig.label}\n </span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">-</span>\n )}\n </td>\n <td className=\"px-4 py-3\">\n {user.notificationsEnabled ? (\n <Bell size={16} className=\"text-success-500\" />\n ) : (\n <BellOff size={16} className=\"text-muted-foreground\" />\n )}\n </td>\n <td className=\"px-4 py-3\">\n {user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-success-500\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-muted-foreground\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n )}\n </td>\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChannelUsersPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/ChannelUsersPage.tsx"],"sourcesContent":["/**\n * Channel Users Page\n *\n * Displays user platform connections for a specific channel\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n User,\n UserCheck,\n Bell,\n BellOff,\n RefreshCw,\n Download,\n} from 'lucide-react';\nimport {\n useChannel,\n type UserPlatformConnection,\n type UserPresenceStatus,\n type PairingStatus,\n} from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportChannelUsers } from '../hooks/useExport';\n\nconst PRESENCE_CONFIG: Record<UserPresenceStatus, { label: string; color: string }> = {\n ONLINE: { label: 'Online', color: 'text-success-500' },\n OFFLINE: { label: 'Offline', color: 'text-muted-foreground' },\n AWAY: { label: 'Away', color: 'text-warning-500' },\n DND: { label: 'Do Not Disturb', color: 'text-error-500' },\n};\n\nconst PAIRING_CONFIG: Record<\n PairingStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: { label: 'Pending', color: 'text-warning-500 bg-warning-100', icon: Clock },\n APPROVED: { label: 'Approved', color: 'text-success-500 bg-success-100', icon: CheckCircle2 },\n EXPIRED: { label: 'Expired', color: 'text-muted-foreground bg-muted', icon: XCircle },\n};\n\nexport function ChannelUsersPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportUsers,\n isExporting: isExportingUsers,\n exportError: exportUsersError,\n } = useExportChannelUsers(organizationId ?? undefined, channelId);\n const [isLoadingUsers, setIsLoadingUsers] = useState(true);\n const [users, setUsers] = useState<UserPlatformConnection[]>([]);\n const [limit, setLimit] = useState(25);\n\n const { channel, isLoading, error, fetchUserPlatformConnections } = useChannel(channelId);\n\n const loadUsers = useCallback(async () => {\n if (!channelId) return;\n setIsLoadingUsers(true);\n try {\n const data = await fetchUserPlatformConnections(limit);\n setUsers(data);\n } finally {\n setIsLoadingUsers(false);\n }\n }, [channelId, fetchUserPlatformConnections, limit]);\n\n useEffect(() => {\n void Promise.resolve().then(() => loadUsers());\n }, [loadUsers]);\n\n const getPresenceConfig = (status: UserPresenceStatus) => {\n return PRESENCE_CONFIG[status] || PRESENCE_CONFIG.OFFLINE;\n };\n\n const getPairingConfig = (status: PairingStatus) => {\n return PAIRING_CONFIG[status] || PAIRING_CONFIG.EXPIRED;\n };\n\n // Loading state\n if (isLoading || isLoadingUsers) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" />\n </div>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-error-100 border border-error-500 rounded-lg\">\n <AlertCircle size={20} className=\"text-error-500\" />\n <div>\n <p className=\"text-sm font-medium text-error-700\">{error || 'Channel not found'}</p>\n </div>\n <button\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-error-700 hover:bg-error-200 rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.USER_PLATFORM_CONNECTION_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-foreground\">\n {tr('channel.channelUsers', 'Channel Users')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {channel.name} - User connections\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-primary/50 text-foreground\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <button\n onClick={loadUsers}\n disabled={isLoadingUsers}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors disabled:opacity-50\"\n >\n <RefreshCw size={16} className={isLoadingUsers ? 'animate-spin' : ''} />\n {tr('channel.refresh', 'Refresh')}\n </button>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_ADMIN}>\n <button\n onClick={() => void exportUsers()}\n disabled={isExportingUsers}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-fg border border-border hover:bg-bg-elevated rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n title={tr('channel.exportCSV', 'Export to CSV')}\n >\n {isExportingUsers ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Download size={16} />\n )}\n {tr('channel.export', 'Export')}\n </button>\n </PermissionGuard>\n </div>\n </div>\n\n {/* Export Error Banner */}\n {exportUsersError && (\n <div className=\"p-4 bg-error-100 border border-error-500 rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-error-500\" />\n <p className=\"text-sm text-error-700 flex-1\">{exportUsersError}</p>\n </div>\n )}\n\n {/* Info Banner */}\n <div className=\"p-4 bg-surface border border-border rounded-lg\">\n <p className=\"text-sm text-muted-foreground\">\n Users connected to this channel through OAuth or pairing. Manage notification\n preferences and DM authorization here.\n </p>\n </div>\n\n {/* Users List */}\n <div className=\"bg-surface border border-border rounded-lg overflow-hidden\">\n <div className=\"overflow-x-auto min-w-full\">\n <table className=\"w-full min-w-[800px]\">\n <thead className=\"bg-muted/50 border-b border-border\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.user', 'User')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.platformId', 'Platform ID')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.source', 'Source')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.status', 'Status')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.pairing', 'Pairing')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.notifications', 'Notifications')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.dmAllowed', 'DM Allowed')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {users.length === 0 ? (\n <tr>\n <td colSpan={7} className=\"px-4 py-8 text-center text-muted-foreground\">\n {tr('channel.noUsersConnected', 'No users connected to this channel')}\n </td>\n </tr>\n ) : (\n users.map((user) => {\n const presenceConfig = getPresenceConfig(\n user.presenceStatus as UserPresenceStatus\n );\n const pairingConfig = user.pairingStatus\n ? getPairingConfig(user.pairingStatus as PairingStatus)\n : null;\n const PairingIcon = pairingConfig?.icon ?? XCircle;\n return (\n <tr key={user.id} className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 bg-accent-primary/10 rounded-full flex items-center justify-center\">\n <User size={16} className=\"text-accent-primary\" />\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {user.externalDisplayName || 'Unknown'}\n </p>\n {user.externalUsername && (\n <p className=\"text-xs text-muted-foreground\">\n {user.externalUsername}\n </p>\n )}\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm font-mono text-muted-foreground\">\n {user.externalUserId}\n </td>\n <td className=\"px-4 py-3\">\n <span className=\"text-xs text-muted-foreground bg-muted px-2 py-1 rounded\">\n {user.source}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`w-2 h-2 rounded-full ${presenceConfig.color.replace('text-', 'bg-')}`}\n />\n <span className={`text-sm ${presenceConfig.color}`}>\n {presenceConfig.label}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3\">\n {user.pairingStatus && pairingConfig ? (\n <span\n className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${pairingConfig.color}`}\n >\n <PairingIcon size={12} />\n {pairingConfig.label}\n </span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">-</span>\n )}\n </td>\n <td className=\"px-4 py-3\">\n {user.notificationsEnabled ? (\n <span className=\"inline-flex items-center gap-1\">\n <Bell size={16} className=\"text-success-500\" aria-hidden=\"true\" />\n <span className=\"sr-only\">{tr('channel.enabled', 'Enabled')}</span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1\">\n <BellOff\n size={16}\n className=\"text-muted-foreground\"\n aria-hidden=\"true\"\n />\n <span className=\"sr-only\">{tr('channel.disabled', 'Disabled')}</span>\n </span>\n )}\n </td>\n <td className=\"px-4 py-3\">\n {user.dmAllowed ? (\n <span className=\"inline-flex items-center gap-1 text-sm text-success-500\">\n <UserCheck size={16} />\n {tr('channel.allowed', 'Allowed')}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-sm text-muted-foreground\">\n <XCircle size={16} />\n {tr('channel.blocked', 'Blocked')}\n </span>\n )}\n </td>\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -16,14 +16,14 @@ import { TagBadge as E } from "@burdenoff/fe-libs/tag";
|
|
|
16
16
|
var D = {
|
|
17
17
|
true: {
|
|
18
18
|
icon: m,
|
|
19
|
-
color: "text-
|
|
20
|
-
bgColor: "bg-
|
|
19
|
+
color: "text-success-500",
|
|
20
|
+
bgColor: "bg-success-500/10",
|
|
21
21
|
label: "Healthy"
|
|
22
22
|
},
|
|
23
23
|
false: {
|
|
24
24
|
icon: x,
|
|
25
|
-
color: "text-
|
|
26
|
-
bgColor: "bg-
|
|
25
|
+
color: "text-error-500",
|
|
26
|
+
bgColor: "bg-error-500/10",
|
|
27
27
|
label: "Unhealthy"
|
|
28
28
|
}
|
|
29
29
|
};
|
|
@@ -214,14 +214,14 @@ function O() {
|
|
|
214
214
|
className: "text-sm text-muted-foreground",
|
|
215
215
|
children: "Status"
|
|
216
216
|
}), /* @__PURE__ */ w("p", {
|
|
217
|
-
className: X.isActive ? "text-
|
|
217
|
+
className: X.isActive ? "text-success-500" : "text-muted-foreground",
|
|
218
218
|
children: X.isActive ? U("channel.activeStatus", "Active") : U("channel.inactiveStatus", "Inactive")
|
|
219
219
|
})] }),
|
|
220
220
|
/* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("p", {
|
|
221
221
|
className: "text-sm text-muted-foreground",
|
|
222
222
|
children: "Token Refresh"
|
|
223
223
|
}), /* @__PURE__ */ w("p", {
|
|
224
|
-
className: X.needsTokenRefresh ? "text-
|
|
224
|
+
className: X.needsTokenRefresh ? "text-warning-500" : "text-muted-foreground",
|
|
225
225
|
children: X.needsTokenRefresh ? U("channel.tokenRefreshRequired", "Required soon") : U("channel.tokenRefreshNotNeeded", "Not needed")
|
|
226
226
|
})] }),
|
|
227
227
|
X.lastTestedAt && /* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("p", {
|
|
@@ -235,7 +235,7 @@ function O() {
|
|
|
235
235
|
className: "text-sm text-muted-foreground",
|
|
236
236
|
children: "Days Until Token Expiry"
|
|
237
237
|
}), /* @__PURE__ */ T("p", {
|
|
238
|
-
className: X.daysUntilExpiry < 30 ? "text-
|
|
238
|
+
className: X.daysUntilExpiry < 30 ? "text-warning-500" : "text-muted-foreground",
|
|
239
239
|
children: [X.daysUntilExpiry, " days"]
|
|
240
240
|
})] })
|
|
241
241
|
]
|