@consilioweb/payload-support 2.0.1 → 3.0.0

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.
Files changed (215) hide show
  1. package/README.md +636 -402
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/index.d.ts +4 -0
  29. package/dist/components/TicketConversation/index.js +2 -2
  30. package/dist/components/TicketConversation/types.d.ts +56 -0
  31. package/dist/components/TicketConversation/utils.d.ts +2 -0
  32. package/dist/index.cjs +392 -320
  33. package/dist/index.d.cts +100 -3
  34. package/dist/index.d.ts +100 -3
  35. package/dist/index.js +388 -321
  36. package/dist/utils/features.d.ts +123 -0
  37. package/dist/utils/features.js +147 -0
  38. package/dist/views/BillingView/client.d.ts +2 -0
  39. package/dist/views/BillingView/index.d.ts +4 -0
  40. package/dist/views/ChatView/client.d.ts +2 -0
  41. package/dist/views/ChatView/client.js +9 -9
  42. package/dist/views/ChatView/index.d.ts +4 -0
  43. package/dist/views/CrmView/client.d.ts +2 -0
  44. package/dist/views/CrmView/client.js +6 -9
  45. package/dist/views/CrmView/index.d.ts +4 -0
  46. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  47. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  48. package/dist/views/ImportConversationView/client.d.ts +2 -0
  49. package/dist/views/ImportConversationView/index.d.ts +4 -0
  50. package/dist/views/LogsView/client.d.ts +2 -0
  51. package/dist/views/LogsView/index.d.ts +4 -0
  52. package/dist/views/NewTicketView/client.d.ts +2 -0
  53. package/dist/views/NewTicketView/index.d.ts +4 -0
  54. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  55. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  56. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  57. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  58. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  59. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  60. package/dist/views/TicketDetailView/client.d.ts +2 -0
  61. package/dist/views/TicketDetailView/client.js +2 -2
  62. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  63. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  64. package/dist/views/TicketDetailView/index.d.ts +4 -0
  65. package/dist/views/TicketDetailView/types.d.ts +40 -0
  66. package/dist/views/TicketInboxView/client.d.ts +2 -0
  67. package/dist/views/TicketInboxView/client.js +5 -5
  68. package/dist/views/TicketInboxView/index.d.ts +4 -0
  69. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  70. package/dist/views/TicketingSettingsView/client.js +5 -4
  71. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  72. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  73. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  74. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  75. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  76. package/dist/views/shared/Skeleton.d.ts +21 -0
  77. package/dist/views/shared/StatusPill.d.ts +8 -0
  78. package/dist/views/shared/adminTokens.d.ts +19 -0
  79. package/dist/views/shared/config.d.ts +8 -0
  80. package/dist/views/shared/config.js +1 -40
  81. package/dist/views/shared/dateLocale.d.ts +5 -0
  82. package/dist/views/shared/index.d.ts +12 -0
  83. package/dist/views/shared/index.js +2 -1
  84. package/dist/views/shared/sla.d.ts +14 -0
  85. package/dist/views.d.ts +13 -13
  86. package/package.json +16 -31
  87. package/src/collections/SupportClients.ts +29 -0
  88. package/src/collections/TicketCollaborators.ts +6 -1
  89. package/src/collections/TicketMessages.ts +46 -24
  90. package/src/collections/Tickets.ts +117 -79
  91. package/src/collections/TimeEntries.ts +57 -32
  92. package/src/components/TicketConversation/config.ts +16 -80
  93. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  94. package/src/components/TicketConversation/index.tsx +2 -2
  95. package/src/endpoints/ai.ts +2 -1
  96. package/src/endpoints/client-intelligence.ts +2 -1
  97. package/src/endpoints/process-scheduled.ts +13 -8
  98. package/src/endpoints/round-robin-config.ts +30 -20
  99. package/src/endpoints/settings.ts +28 -77
  100. package/src/index.ts +6 -2
  101. package/src/portal/LiveChat.tsx +33 -22
  102. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  103. package/src/types/lucide-react.d.ts +10 -2
  104. package/src/utils/aiAgent.ts +2 -1
  105. package/src/utils/aiProvider.ts +21 -0
  106. package/src/utils/features.ts +297 -0
  107. package/src/utils/fireWebhooks.ts +6 -0
  108. package/src/utils/generateTicketSynthesis.ts +2 -1
  109. package/src/utils/index.ts +4 -2
  110. package/src/utils/readSettings.ts +82 -9
  111. package/src/utils/webhookDispatcher.ts +19 -7
  112. package/src/views/ChatView/client.tsx +13 -9
  113. package/src/views/CrmView/client.tsx +10 -11
  114. package/src/views/TicketDetailView/client.tsx +2 -2
  115. package/src/views/TicketInboxView/client.tsx +10 -5
  116. package/src/views/TicketingSettingsView/client.tsx +8 -6
  117. package/src/views/shared/config.ts +14 -80
  118. package/src/views/shared/index.ts +3 -1
  119. package/dist/components/RichTextEditor/index.cjs +0 -279
  120. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  121. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  122. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  123. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  124. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  125. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  126. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  127. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  128. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  129. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  130. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  131. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  132. package/dist/components/TicketConversation/config.cjs +0 -44
  133. package/dist/components/TicketConversation/constants.cjs +0 -127
  134. package/dist/components/TicketConversation/context.cjs +0 -13
  135. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  136. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  137. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  138. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  139. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  140. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  141. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -101
  142. package/dist/components/TicketConversation/index.cjs +0 -1114
  143. package/dist/components/TicketConversation/types.cjs +0 -2
  144. package/dist/components/TicketConversation/utils.cjs +0 -27
  145. package/dist/views/BillingView/client.cjs +0 -469
  146. package/dist/views/BillingView/index.cjs +0 -34
  147. package/dist/views/ChatView/client.cjs +0 -301
  148. package/dist/views/ChatView/index.cjs +0 -34
  149. package/dist/views/CrmView/client.cjs +0 -458
  150. package/dist/views/CrmView/index.cjs +0 -34
  151. package/dist/views/EmailTrackingView/client.cjs +0 -171
  152. package/dist/views/EmailTrackingView/index.cjs +0 -34
  153. package/dist/views/ImportConversationView/client.cjs +0 -238
  154. package/dist/views/ImportConversationView/index.cjs +0 -34
  155. package/dist/views/LogsView/client.cjs +0 -148
  156. package/dist/views/LogsView/index.cjs +0 -32
  157. package/dist/views/NewTicketView/client.cjs +0 -213
  158. package/dist/views/NewTicketView/index.cjs +0 -32
  159. package/dist/views/PendingEmailsView/client.cjs +0 -471
  160. package/dist/views/PendingEmailsView/index.cjs +0 -34
  161. package/dist/views/SupportDashboardView/client.cjs +0 -437
  162. package/dist/views/SupportDashboardView/index.cjs +0 -34
  163. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  164. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  165. package/dist/views/TicketDetailView/client.cjs +0 -1412
  166. package/dist/views/TicketDetailView/constants.cjs +0 -20
  167. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  168. package/dist/views/TicketDetailView/index.cjs +0 -34
  169. package/dist/views/TicketDetailView/types.cjs +0 -2
  170. package/dist/views/TicketInboxView/client.cjs +0 -359
  171. package/dist/views/TicketInboxView/index.cjs +0 -32
  172. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  173. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  174. package/dist/views/TimeDashboardView/client.cjs +0 -180
  175. package/dist/views/TimeDashboardView/index.cjs +0 -34
  176. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  177. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  178. package/dist/views/shared/Skeleton.cjs +0 -77
  179. package/dist/views/shared/StatusPill.cjs +0 -47
  180. package/dist/views/shared/adminTokens.cjs +0 -34
  181. package/dist/views/shared/config.cjs +0 -44
  182. package/dist/views/shared/dateLocale.cjs +0 -5
  183. package/dist/views/shared/index.cjs +0 -72
  184. package/dist/views/shared/sla.cjs +0 -37
  185. package/dist/views.cjs +0 -86
  186. package/dist/views.d.cts +0 -13
  187. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  188. package/src/__tests__/authResponses.test.ts +0 -69
  189. package/src/__tests__/capabilities.test.ts +0 -71
  190. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  191. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  192. package/src/__tests__/integration/auth.test.ts +0 -43
  193. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  194. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  195. package/src/__tests__/integration/channels.test.ts +0 -52
  196. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  197. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  198. package/src/__tests__/integration/digest.test.ts +0 -48
  199. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  200. package/src/__tests__/integration/isolation.test.ts +0 -81
  201. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  202. package/src/__tests__/integration/nps.test.ts +0 -37
  203. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  204. package/src/__tests__/integration/push.test.ts +0 -75
  205. package/src/__tests__/integration/sanitization.test.ts +0 -35
  206. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  207. package/src/__tests__/integration/smoke.test.ts +0 -26
  208. package/src/__tests__/integration/snooze.test.ts +0 -68
  209. package/src/__tests__/integration/teams.test.ts +0 -38
  210. package/src/__tests__/rateLimiter.test.ts +0 -83
  211. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  212. package/src/__tests__/sla.test.ts +0 -258
  213. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  214. package/src/__tests__/translations.test.ts +0 -67
  215. package/src/__tests__/webhookSecurity.test.ts +0 -58
@@ -1,9 +1,24 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireAdmin, handleAuthError } from '../utils/auth'
4
- import { dbFind } from '../utils/db'
4
+ import {
5
+ SUPPORT_SETTINGS_PREF_KEY as PREF_KEY,
6
+ invalidateSupportSettingsCache,
7
+ mergeSupportSettings,
8
+ readSupportSettingsState,
9
+ } from '../utils/readSettings'
10
+ import { stripProjectedFeatures } from '../utils/features'
5
11
 
6
- const PREF_KEY = 'support-round-robin'
12
+ /**
13
+ * Round-robin used to live in its own `support-round-robin` preference row,
14
+ * while the admin UI toggled a `roundRobin` flag in `localStorage` — the two
15
+ * never met, so the toggle had no effect on assignment.
16
+ *
17
+ * Both endpoints below now read and write `settings.features.roundRobin`, the
18
+ * same value `/api/support/settings` exposes and `Tickets`' auto-assign hook
19
+ * reads. The legacy row is still honoured on read until the first save (see
20
+ * `readSupportSettingsState`), so an install that had it enabled keeps it.
21
+ */
7
22
 
8
23
  /**
9
24
  * GET /api/support/round-robin-config — Get round-robin enabled status
@@ -18,18 +33,9 @@ export function createRoundRobinConfigGetEndpoint(slugs: CollectionSlugs): Endpo
18
33
 
19
34
  requireAdmin(req, slugs)
20
35
 
21
- const prefs = await dbFind(payload, 'payload-preferences', {
22
- where: { key: { equals: PREF_KEY } },
23
- limit: 1,
24
- depth: 0,
25
- overrideAccess: true,
26
- })
27
-
28
- const enabled = prefs.docs.length > 0
29
- ? (prefs.docs[0].value as { enabled?: boolean })?.enabled === true
30
- : false
36
+ const { settings } = await readSupportSettingsState(payload)
31
37
 
32
- return Response.json({ enabled })
38
+ return Response.json({ enabled: settings.features.roundRobin })
33
39
  } catch (error) {
34
40
  const authResponse = handleAuthError(error)
35
41
  if (authResponse) return authResponse
@@ -55,19 +61,21 @@ export function createRoundRobinConfigPostEndpoint(slugs: CollectionSlugs): Endp
55
61
 
56
62
  const { enabled } = (await req.json!()) as { enabled: boolean }
57
63
 
58
- const existing = await dbFind(payload, 'payload-preferences', {
59
- where: { key: { equals: PREF_KEY } },
60
- limit: 1,
61
- depth: 0,
62
- overrideAccess: true,
63
- })
64
+ const { settings: current } = await readSupportSettingsState(payload)
65
+ const merged = mergeSupportSettings(
66
+ { features: { ...current.features, roundRobin: !!enabled } },
67
+ current,
68
+ )
64
69
 
65
70
  await payload.db.upsert({
66
71
  collection: 'payload-preferences',
67
72
  data: {
68
73
  key: PREF_KEY,
69
74
  user: { relationTo: req.user!.collection, value: req.user!.id },
70
- value: { enabled: !!enabled },
75
+ value: {
76
+ ...merged,
77
+ features: stripProjectedFeatures(merged.features),
78
+ } as unknown as Record<string, unknown>,
71
79
  },
72
80
  req: { payload, user: req.user } as any,
73
81
  where: {
@@ -79,6 +87,8 @@ export function createRoundRobinConfigPostEndpoint(slugs: CollectionSlugs): Endp
79
87
  },
80
88
  })
81
89
 
90
+ invalidateSupportSettingsCache()
91
+
82
92
  return Response.json({ enabled: !!enabled })
83
93
  } catch (error) {
84
94
  const authResponse = handleAuthError(error)
@@ -1,59 +1,21 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireAdmin, handleAuthError } from '../utils/auth'
4
- import { invalidateSupportSettingsCache } from '../utils/readSettings'
5
- import { dbFind } from '../utils/db'
6
-
7
- const PREF_KEY = 'support-settings'
8
-
9
- interface SupportSettings {
10
- email: {
11
- fromAddress: string
12
- fromName: string
13
- replyToAddress: string
14
- }
15
- ai: {
16
- provider: 'anthropic' | 'openai' | 'gemini' | 'ollama'
17
- model: string
18
- enableSentiment: boolean
19
- enableSynthesis: boolean
20
- enableSuggestion: boolean
21
- enableRewrite: boolean
22
- }
23
- sla: {
24
- firstResponseMinutes: number
25
- resolutionMinutes: number
26
- businessHoursOnly: boolean
27
- escalationEmail: string
28
- }
29
- autoClose: {
30
- enabled: boolean
31
- daysBeforeClose: number
32
- reminderDaysBefore: number
33
- }
34
- }
35
-
36
- const DEFAULT_SUPPORT_SETTINGS: SupportSettings = {
37
- email: { fromAddress: '', fromName: 'Support', replyToAddress: '' },
38
- ai: {
39
- provider: 'anthropic',
40
- model: 'claude-haiku-4-5-20251001',
41
- enableSentiment: true,
42
- enableSynthesis: true,
43
- enableSuggestion: true,
44
- enableRewrite: true,
45
- },
46
- sla: {
47
- firstResponseMinutes: 120,
48
- resolutionMinutes: 1440,
49
- businessHoursOnly: true,
50
- escalationEmail: '',
51
- },
52
- autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
53
- }
4
+ import {
5
+ SUPPORT_SETTINGS_PREF_KEY as PREF_KEY,
6
+ invalidateSupportSettingsCache,
7
+ mergeSupportSettings,
8
+ readSupportSettingsState,
9
+ type SupportSettings,
10
+ } from '../utils/readSettings'
11
+ import { stripProjectedFeatures } from '../utils/features'
54
12
 
55
13
  /**
56
14
  * GET /api/support/settings — Read support settings
15
+ *
16
+ * `featuresConfigured` tells the admin UI whether the server has ever had
17
+ * feature flags saved; a client still holding pre-2.1 `localStorage` flags uses
18
+ * it to seed the server once instead of losing its configuration.
57
19
  */
58
20
  export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
59
21
  return {
@@ -65,25 +27,9 @@ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
65
27
 
66
28
  requireAdmin(req, slugs)
67
29
 
68
- const prefs = await dbFind(payload, 'payload-preferences', {
69
- where: { key: { equals: PREF_KEY } },
70
- limit: 1,
71
- depth: 0,
72
- overrideAccess: true,
73
- })
74
-
75
- let settings = { ...DEFAULT_SUPPORT_SETTINGS }
76
- if (prefs.docs.length > 0) {
77
- const stored = prefs.docs[0].value as Partial<SupportSettings>
78
- settings = {
79
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
80
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
81
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
82
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose },
83
- }
84
- }
30
+ const { settings, featuresConfigured } = await readSupportSettingsState(payload)
85
31
 
86
- return Response.json(settings)
32
+ return Response.json({ ...settings, featuresConfigured })
87
33
  } catch (error) {
88
34
  const authResponse = handleAuthError(error)
89
35
  if (authResponse) return authResponse
@@ -95,7 +41,11 @@ export function createSettingsGetEndpoint(slugs: CollectionSlugs): Endpoint {
95
41
  }
96
42
 
97
43
  /**
98
- * POST /api/support/settings — Save all support settings (admin-only)
44
+ * POST /api/support/settings — Save support settings (admin-only)
45
+ *
46
+ * The body is merged onto the CURRENT settings, not onto the defaults: the
47
+ * feature-flag screen posts `{ features }` alone, and that must not reset the
48
+ * email / AI / SLA blocks.
99
49
  */
100
50
  export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
101
51
  return {
@@ -109,19 +59,20 @@ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
109
59
 
110
60
  const body = (await req.json!()) as Partial<SupportSettings>
111
61
 
112
- const merged: SupportSettings = {
113
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...body.email },
114
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
115
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
116
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose },
117
- }
62
+ const { settings: current } = await readSupportSettingsState(payload)
63
+ const merged = mergeSupportSettings(body, current)
118
64
 
119
65
  await payload.db.upsert({
120
66
  collection: 'payload-preferences',
121
67
  data: {
122
68
  key: PREF_KEY,
123
69
  user: { relationTo: req.user!.collection, value: req.user!.id },
124
- value: merged as unknown as Record<string, unknown>,
70
+ value: {
71
+ ...merged,
72
+ // Persist the flags without the two projected ones, so `features`
73
+ // and the `autoClose` block can never drift apart.
74
+ features: stripProjectedFeatures(merged.features),
75
+ } as unknown as Record<string, unknown>,
125
76
  },
126
77
  req: { payload, user: req.user } as any,
127
78
  where: {
@@ -135,7 +86,7 @@ export function createSettingsPostEndpoint(slugs: CollectionSlugs): Endpoint {
135
86
 
136
87
  invalidateSupportSettingsCache()
137
88
 
138
- return Response.json(merged)
89
+ return Response.json({ ...merged, featuresConfigured: true })
139
90
  } catch (error) {
140
91
  const authResponse = handleAuthError(error)
141
92
  if (authResponse) return authResponse
package/src/index.ts CHANGED
@@ -21,8 +21,12 @@ export { DEFAULT_FEATURES } from './types'
21
21
  // Utils
22
22
  export { resolveSlugs, DEFAULT_SLUGS } from './utils/slugs'
23
23
  export type { CollectionSlugs } from './utils/slugs'
24
- export { readSupportSettings, readUserPrefs, DEFAULT_SETTINGS, DEFAULT_USER_PREFS } from './utils/readSettings'
25
- export type { SupportSettings, UserPrefs } from './utils/readSettings'
24
+ export { readSupportSettings, readSupportSettingsState, readUserPrefs, DEFAULT_SETTINGS, DEFAULT_USER_PREFS } from './utils/readSettings'
25
+ export type { SupportSettings, SupportSettingsState, UserPrefs } from './utils/readSettings'
26
+ // Runtime feature flags (server-authoritative, see utils/features.ts). Distinct
27
+ // from the build-time `SupportFeatures` passed to `supportPlugin()`.
28
+ export { DEFAULT_TICKETING_FEATURES, normalizeFeatures, projectAutoClose, stripProjectedFeatures } from './utils/features'
29
+ export type { TicketingFeatures } from './utils/features'
26
30
  export { MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter } from './utils/rateLimiter'
27
31
  export type { RateLimitEntry, RateLimitStore } from './utils/rateLimiter'
28
32
  export { DEFAULT_INBOUND_EMAIL_LIMITS, validateInboundEmailPayload, verifySecret } from './utils/webhookSecurity'
@@ -29,6 +29,13 @@ export function LiveChat() {
29
29
  const lastFetchRef = useRef<string | null>(null)
30
30
  const inputRef = useRef<HTMLInputElement>(null)
31
31
  const pollIntervalRef = useRef<ReturnType<typeof setTimeout> | null>(null)
32
+ // Mirror of `messages`. The poll decision (and the unread badge) MUST be taken
33
+ // outside the setMessages updater: React skips the updater on the eager-state
34
+ // path, so a flag assigned inside it is only ever written on the very first
35
+ // update — after which `hasNewMessages` stays false, the adaptive backoff climbs
36
+ // to its 30 s ceiling and never comes back down, mid-conversation. Same pattern
37
+ // as ChatWidget.tsx.
38
+ const messagesRef = useRef<ChatMessage[]>([])
32
39
 
33
40
  // Load session from localStorage on mount
34
41
  useEffect(() => {
@@ -42,6 +49,7 @@ export function LiveChat() {
42
49
 
43
50
  // Auto-scroll to bottom when messages change
44
51
  useEffect(() => {
52
+ messagesRef.current = messages
45
53
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
46
54
  }, [messages])
47
55
 
@@ -61,31 +69,32 @@ export function LiveChat() {
61
69
 
62
70
  const data = await res.json()
63
71
  if (data.messages?.length > 0) {
64
- let hadNewMessages = false
65
- setMessages((prev) => {
66
- const existingIds = new Set(prev.map((m) => String(m.id)))
67
- const newMsgs = data.messages.filter(
68
- (m: ChatMessage) => !existingIds.has(String(m.id)),
69
- )
70
- if (newMsgs.length === 0) return prev
71
-
72
- hadNewMessages = true
73
-
74
- // Count agent messages as unread if chat panel is closed
75
- if (screen === 'closed') {
76
- const agentNewMsgs = newMsgs.filter(
77
- (m: ChatMessage) => m.senderType === 'agent',
78
- )
79
- if (agentNewMsgs.length > 0) {
80
- setUnreadCount((prev) => prev + agentNewMsgs.length)
81
- }
72
+ const existingIds = new Set(messagesRef.current.map((m) => String(m.id)))
73
+ const newMsgs: ChatMessage[] = data.messages.filter(
74
+ (m: ChatMessage) => !existingIds.has(String(m.id)),
75
+ )
76
+ lastFetchRef.current = data.messages[data.messages.length - 1].createdAt
77
+ if (newMsgs.length === 0) return false
78
+
79
+ // Count agent messages as unread if chat panel is closed. Kept OUT of the
80
+ // updater: nesting a setState there also double-counted the badge under
81
+ // StrictMode's double invocation in dev.
82
+ if (screen === 'closed') {
83
+ const agentNewMsgs = newMsgs.filter((m) => m.senderType === 'agent')
84
+ if (agentNewMsgs.length > 0) {
85
+ setUnreadCount((prev) => prev + agentNewMsgs.length)
82
86
  }
87
+ }
83
88
 
84
- return [...prev, ...newMsgs]
89
+ setMessages((prev) => {
90
+ // Re-dedupe against the authoritative state: the mirror can lag by one
91
+ // commit if a message was appended optimistically in the meantime.
92
+ const prevIds = new Set(prev.map((m) => String(m.id)))
93
+ const trulyNew = newMsgs.filter((m) => !prevIds.has(String(m.id)))
94
+ if (trulyNew.length === 0) return prev
95
+ return [...prev, ...trulyNew]
85
96
  })
86
- lastFetchRef.current =
87
- data.messages[data.messages.length - 1].createdAt
88
- return hadNewMessages
97
+ return true
89
98
  }
90
99
  return false
91
100
  } catch {
@@ -127,6 +136,8 @@ export function LiveChat() {
127
136
  // If we have a session, fetch messages
128
137
  if (session.sessionId) {
129
138
  lastFetchRef.current = null
139
+ // Synchronous too: pollMessages() runs before the mirroring effect commits.
140
+ messagesRef.current = []
130
141
  setMessages([])
131
142
  pollMessages()
132
143
  }
@@ -30,8 +30,12 @@ export function TicketPolling({
30
30
 
31
31
  const interval = setInterval(async () => {
32
32
  try {
33
+ // `/count` returns a single number. The previous limit=0 idiom did NOT return
34
+ // zero documents — Payload disables pagination for limit=0 and derives
35
+ // totalDocs from the rows it materialised, so every open portal tab used to
36
+ // re-download its whole conversation every 30 s to read one counter.
33
37
  const res = await fetch(
34
- `/api/ticket-messages?where[ticket][equals]=${ticketId}&limit=0&depth=0`,
38
+ `/api/ticket-messages/count?where[ticket][equals]=${ticketId}`,
35
39
  { credentials: 'include' },
36
40
  )
37
41
  if (res.status === 401 || res.status === 403) {
@@ -1,7 +1,15 @@
1
1
  /**
2
2
  * Ambient stub for 'lucide-react'.
3
- * lucide-react is an optional peer dependency — the host application installs it.
4
- * This stub satisfies TypeScript when the plugin is type-checked in isolation.
3
+ *
4
+ * lucide-react is a REQUIRED peer dependency three unconditionally registered
5
+ * components import it statically, so a host without it fails at build time.
6
+ * It is deliberately not a devDependency here; this stub is what lets the plugin
7
+ * type-check in isolation without pulling the icon set into the repo.
8
+ *
9
+ * Keep the icon list below in sync with the actual imports:
10
+ * src/portal/LiveChat.tsx
11
+ * src/views/PendingEmailsView/client.tsx
12
+ * src/views/TicketingSettingsView/client.tsx
5
13
  */
6
14
  declare module 'lucide-react' {
7
15
  import type { FC, SVGProps } from 'react'
@@ -2,6 +2,7 @@ import type { Payload } from 'payload'
2
2
  import type { CollectionSlugs } from './slugs'
3
3
  import { readSupportSettings, type SupportSettings } from './readSettings'
4
4
  import { dbFind, dbFindByID, dbCreate, dbUpdate } from './db'
5
+ import { resolveOllamaBaseUrl } from './aiProvider'
5
6
 
6
7
  export interface AgentDecision {
7
8
  action: 'reply' | 'escalate'
@@ -61,7 +62,7 @@ export async function applyAgentDecision(
61
62
  function getClient(aiSettings: SupportSettings['ai']) {
62
63
  const Anthropic = require('@anthropic-ai/sdk').default
63
64
  if (aiSettings.provider === 'ollama') {
64
- return new Anthropic({ apiKey: 'ollama', baseURL: process.env.OLLAMA_API_URL || 'https://ollama.orkelis.app/v1' })
65
+ return new Anthropic({ apiKey: 'ollama', baseURL: resolveOllamaBaseUrl() })
65
66
  }
66
67
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
67
68
  }
@@ -0,0 +1,21 @@
1
+ import { APIError } from 'payload'
2
+
3
+ /**
4
+ * Resolve the base URL of the OpenAI-compatible gateway used by the `ollama`
5
+ * provider.
6
+ *
7
+ * There is deliberately NO default here. An integrator who selects `ollama` does
8
+ * so precisely to keep ticket content off third-party infrastructure; a silent
9
+ * fallback to any hosted endpoint would ship the whole conversation — subject,
10
+ * message bodies, client name and company — to a server the operator never chose,
11
+ * and would do so invisibly because the AI answer still comes back normally.
12
+ *
13
+ * Fail loudly instead: a missing `OLLAMA_API_URL` is a configuration error.
14
+ */
15
+ export function resolveOllamaBaseUrl(): string {
16
+ const baseURL = process.env.OLLAMA_API_URL
17
+ if (!baseURL) {
18
+ throw new APIError("provider 'ollama' requires OLLAMA_API_URL", 500)
19
+ }
20
+ return baseURL
21
+ }