@open-mercato/core 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84

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 (130) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/authFixtures.js.map +1 -1
  3. package/dist/helpers/integration/authUi.js +10 -2
  4. package/dist/helpers/integration/authUi.js.map +2 -2
  5. package/dist/helpers/integration/salesUi.js +3 -1
  6. package/dist/helpers/integration/salesUi.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/Explorer.js +10 -1
  8. package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
  9. package/dist/modules/audit_logs/lib/display-helpers.js +28 -27
  10. package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +21 -3
  12. package/dist/modules/auth/api/users/route.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +6 -1
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/backend/users/create/page.js +4 -1
  16. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  17. package/dist/modules/auth/backend/users/page.js +56 -2
  18. package/dist/modules/auth/backend/users/page.js.map +3 -3
  19. package/dist/modules/auth/commands/users.js +14 -0
  20. package/dist/modules/auth/commands/users.js.map +2 -2
  21. package/dist/modules/auth/data/validators.js +8 -0
  22. package/dist/modules/auth/data/validators.js.map +2 -2
  23. package/dist/modules/catalog/ai-agents.js +4 -0
  24. package/dist/modules/catalog/ai-agents.js.map +2 -2
  25. package/dist/modules/customers/ai-agents.js.map +2 -2
  26. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
  27. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  28. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -9
  29. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  30. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -3
  31. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  32. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
  33. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  34. package/dist/modules/customers/components/PipelineSettings.js +108 -99
  35. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  36. package/dist/modules/customers/components/detail/AssignRoleDialog.js +13 -5
  37. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  38. package/dist/modules/customers/components/detail/EntityTagsDialog.js +10 -2
  39. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
  40. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +15 -8
  41. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  42. package/dist/modules/customers/components/detail/RolesSection.js +35 -22
  43. package/dist/modules/customers/components/detail/RolesSection.js.map +2 -2
  44. package/dist/modules/customers/components/detail/TasksSection.js +2 -2
  45. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  46. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +73 -64
  47. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  48. package/dist/modules/customers/components/linking/LinkEntityDialog.js +11 -2
  49. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
  50. package/dist/modules/data_sync/backend/data-sync/page.js +2 -9
  51. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  52. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +14 -29
  53. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  54. package/dist/modules/dictionaries/components/DictionariesManager.js +10 -2
  55. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  56. package/dist/modules/entities/components/EncryptionManager.js +40 -17
  57. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  58. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  59. package/dist/modules/integrations/backend/integrations/[id]/page.js +79 -95
  60. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  61. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +49 -70
  62. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  63. package/dist/modules/planner/components/AvailabilityRuleSetForm.js +1 -1
  64. package/dist/modules/planner/components/AvailabilityRuleSetForm.js.map +1 -1
  65. package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -19
  66. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  67. package/dist/modules/resources/components/ResourceCrudForm.js +1 -1
  68. package/dist/modules/resources/components/ResourceCrudForm.js.map +1 -1
  69. package/dist/modules/resources/components/ResourceTypeCrudForm.js +1 -1
  70. package/dist/modules/resources/components/ResourceTypeCrudForm.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ReturnDialog.js +11 -8
  72. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  73. package/dist/modules/staff/backend/staff/profile/create/page.js +1 -1
  74. package/dist/modules/staff/backend/staff/profile/create/page.js.map +2 -2
  75. package/dist/modules/staff/components/TeamForm.js +1 -1
  76. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  77. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  78. package/dist/modules/staff/components/TeamMemberForm.js.map +1 -1
  79. package/dist/modules/staff/components/TeamRoleForm.js +1 -1
  80. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  81. package/package.json +5 -5
  82. package/src/helpers/integration/authFixtures.ts +1 -1
  83. package/src/helpers/integration/authUi.ts +11 -2
  84. package/src/helpers/integration/salesUi.ts +10 -1
  85. package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +7 -3
  86. package/src/modules/audit_logs/lib/display-helpers.tsx +22 -14
  87. package/src/modules/auth/api/users/route.ts +22 -3
  88. package/src/modules/auth/backend/users/[id]/edit/page.tsx +9 -1
  89. package/src/modules/auth/backend/users/create/page.tsx +6 -2
  90. package/src/modules/auth/backend/users/page.tsx +60 -3
  91. package/src/modules/auth/commands/users.ts +15 -0
  92. package/src/modules/auth/data/validators.ts +8 -0
  93. package/src/modules/auth/i18n/de.json +4 -0
  94. package/src/modules/auth/i18n/en.json +4 -0
  95. package/src/modules/auth/i18n/es.json +4 -0
  96. package/src/modules/auth/i18n/pl.json +4 -0
  97. package/src/modules/catalog/ai-agents.ts +8 -49
  98. package/src/modules/customers/ai-agents.ts +4 -51
  99. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
  100. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +12 -13
  101. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +17 -13
  102. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
  103. package/src/modules/customers/components/PipelineSettings.tsx +95 -95
  104. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +7 -4
  105. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +13 -8
  106. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +16 -12
  107. package/src/modules/customers/components/detail/RolesSection.tsx +34 -31
  108. package/src/modules/customers/components/detail/TasksSection.tsx +16 -8
  109. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +77 -51
  110. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +7 -3
  111. package/src/modules/data_sync/backend/data-sync/page.tsx +4 -7
  112. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -54
  113. package/src/modules/dictionaries/components/DictionariesManager.tsx +7 -4
  114. package/src/modules/entities/components/EncryptionManager.tsx +50 -21
  115. package/src/modules/inbox_ops/lib/llmProvider.ts +10 -0
  116. package/src/modules/integrations/backend/integrations/[id]/page.tsx +101 -146
  117. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +60 -103
  118. package/src/modules/planner/components/AvailabilityRuleSetForm.tsx +1 -1
  119. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +17 -17
  120. package/src/modules/resources/components/ResourceCrudForm.tsx +1 -1
  121. package/src/modules/resources/components/ResourceTypeCrudForm.tsx +1 -1
  122. package/src/modules/sales/components/documents/ReturnDialog.tsx +10 -7
  123. package/src/modules/sales/i18n/de.json +2 -0
  124. package/src/modules/sales/i18n/en.json +2 -0
  125. package/src/modules/sales/i18n/es.json +2 -0
  126. package/src/modules/sales/i18n/pl.json +2 -0
  127. package/src/modules/staff/backend/staff/profile/create/page.tsx +1 -1
  128. package/src/modules/staff/components/TeamForm.tsx +1 -1
  129. package/src/modules/staff/components/TeamMemberForm.tsx +1 -1
  130. package/src/modules/staff/components/TeamRoleForm.tsx +1 -1
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useQuery, useMutation } from '@tanstack/react-query'
5
5
  import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
6
7
  import {
7
8
  Select,
8
9
  SelectContent,
@@ -14,13 +15,14 @@ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
14
15
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
16
  import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
16
17
  import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
17
- import { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'
18
+ import { useCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
18
19
  import { Plus, Save, Trash2 } from 'lucide-react'
19
20
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
20
21
  import { useT } from '@open-mercato/shared/lib/i18n/context'
21
22
  import { getEntityFields } from '#generated/entity-fields-registry'
22
23
 
23
24
  type EntityOption = { entityId: string; label?: string; source?: string }
25
+ type FieldOption = { value: string; label: string }
24
26
 
25
27
  type EncryptionFieldRow = {
26
28
  id: string
@@ -36,6 +38,8 @@ type EncryptionMapResponse = {
36
38
 
37
39
  type CanonicalOption = { value: string; label?: string }
38
40
 
41
+ const EMPTY_FIELD_DEFS: CustomFieldDefDto[] = []
42
+
39
43
  function normalizeToken(value: string): string {
40
44
  return value
41
45
  .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
@@ -64,6 +68,24 @@ function normalizeFieldRows(raw: EncryptionMapResponse | undefined, options: Can
64
68
  }))
65
69
  }
66
70
 
71
+ function areFieldRowsEqual(a: EncryptionFieldRow[], b: EncryptionFieldRow[]): boolean {
72
+ if (a === b) return true
73
+ if (a.length !== b.length) return false
74
+ return a.every((row, idx) => {
75
+ const other = b[idx]
76
+ return !!other && row.id === other.id && row.field === other.field && (row.hashField ?? null) === (other.hashField ?? null)
77
+ })
78
+ }
79
+
80
+ function areFieldOptionsEqual(a: FieldOption[], b: FieldOption[]): boolean {
81
+ if (a === b) return true
82
+ if (a.length !== b.length) return false
83
+ return a.every((option, idx) => {
84
+ const other = b[idx]
85
+ return !!other && option.value === other.value && option.label === other.label
86
+ })
87
+ }
88
+
67
89
  function buildRowId(): string {
68
90
  if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()
69
91
  return `row-${Math.random().toString(36).slice(2)}`
@@ -75,11 +97,12 @@ export function EncryptionManager() {
75
97
  const [selectedEntityId, setSelectedEntityId] = React.useState('')
76
98
  const [fields, setFields] = React.useState<EncryptionFieldRow[]>([])
77
99
  const [isActive, setIsActive] = React.useState(true)
78
- const [baseFieldOptions, setBaseFieldOptions] = React.useState<Array<{ value: string; label: string }>>([])
100
+ const [baseFieldOptions, setBaseFieldOptions] = React.useState<FieldOption[]>([])
79
101
  const [hasUserEdited, setHasUserEdited] = React.useState(false)
80
- const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {
102
+ const { data: rawFieldDefs, isLoading: loadingFieldDefs } = useCustomFieldDefs(selectedEntityId ? [selectedEntityId] : [], {
81
103
  enabled: !!selectedEntityId,
82
104
  })
105
+ const fieldDefs = rawFieldDefs ?? EMPTY_FIELD_DEFS
83
106
  const canonicalOptions = React.useMemo<CanonicalOption[]>(() => {
84
107
  const options: CanonicalOption[] = []
85
108
  for (const option of baseFieldOptions) {
@@ -133,13 +156,13 @@ export function EncryptionManager() {
133
156
 
134
157
  React.useEffect(() => {
135
158
  if (!selectedEntityId) {
136
- setBaseFieldOptions([])
159
+ setBaseFieldOptions((prev) => (prev.length ? [] : prev))
137
160
  return
138
161
  }
139
162
  const parts = selectedEntityId.split(':')
140
163
  const entitySlug = parts[1]
141
164
  if (!entitySlug) {
142
- setBaseFieldOptions([])
165
+ setBaseFieldOptions((prev) => (prev.length ? [] : prev))
143
166
  return
144
167
  }
145
168
 
@@ -147,11 +170,11 @@ export function EncryptionManager() {
147
170
  const mod = getEntityFields(entitySlug)
148
171
  if (!mod) {
149
172
  console.warn('[encryption] No fields found for entity', entitySlug)
150
- setBaseFieldOptions([])
173
+ setBaseFieldOptions((prev) => (prev.length ? [] : prev))
151
174
  return
152
175
  }
153
176
 
154
- const options: Array<{ value: string; label: string }> = []
177
+ const options: FieldOption[] = []
155
178
  for (const raw of Object.values(mod)) {
156
179
  if (typeof raw !== 'string' || !raw.trim()) continue
157
180
  const value = raw.trim()
@@ -163,7 +186,7 @@ export function EncryptionManager() {
163
186
  .trim() || value
164
187
  options.push({ value, label })
165
188
  }
166
- setBaseFieldOptions(options)
189
+ setBaseFieldOptions((prev) => (areFieldOptionsEqual(prev, options) ? prev : options))
167
190
  }, [selectedEntityId])
168
191
 
169
192
  const {
@@ -188,14 +211,16 @@ export function EncryptionManager() {
188
211
  const isSameMap = signature === lastMapSignatureRef.current
189
212
  if (isSameMap && hasUserEdited) return
190
213
  if (!map) {
191
- setFields([])
192
- setIsActive(true)
214
+ setFields((prev) => (prev.length ? [] : prev))
215
+ setIsActive((prev) => (prev === true ? prev : true))
193
216
  setHasUserEdited(false)
194
217
  lastMapSignatureRef.current = signature
195
218
  return
196
219
  }
197
- setFields(normalizeFieldRows(map, canonicalOptions))
198
- setIsActive(map?.isActive !== false)
220
+ const nextFields = normalizeFieldRows(map, canonicalOptions)
221
+ setFields((prev) => (areFieldRowsEqual(prev, nextFields) ? prev : nextFields))
222
+ const nextIsActive = map?.isActive !== false
223
+ setIsActive((prev) => (prev === nextIsActive ? prev : nextIsActive))
199
224
  setHasUserEdited(false)
200
225
  lastMapSignatureRef.current = signature
201
226
  }, [mapSignature, map, canonicalOptions, hasUserEdited])
@@ -267,7 +292,7 @@ export function EncryptionManager() {
267
292
  <ErrorMessage
268
293
  label={t('entities.encryption.errors.loadMap', 'Failed to load encryption map')}
269
294
  action={(
270
- <Button variant="outline" size="sm" onClick={() => void refetchMap()}>
295
+ <Button type="button" variant="outline" size="sm" onClick={() => void refetchMap()}>
271
296
  {t('entities.encryption.actions.retry', 'Retry')}
272
297
  </Button>
273
298
  )}
@@ -348,6 +373,7 @@ export function EncryptionManager() {
348
373
  </td>
349
374
  <td className="px-3 py-2 align-top text-right">
350
375
  <Button
376
+ type="button"
351
377
  variant="ghost"
352
378
  size="sm"
353
379
  className="h-9 w-9 px-0"
@@ -398,7 +424,7 @@ export function EncryptionManager() {
398
424
  </SelectContent>
399
425
  </Select>
400
426
  {entitiesError ? (
401
- <p className="mt-1 text-xs text-red-600">
427
+ <p className="mt-1 text-xs text-status-error-text">
402
428
  {t('entities.encryption.errors.loadEntities', 'Failed to load entities')}
403
429
  </p>
404
430
  ) : (
@@ -407,14 +433,16 @@ export function EncryptionManager() {
407
433
  </p>
408
434
  )}
409
435
  </div>
410
- <label className="inline-flex items-center gap-2 text-sm">
411
- <input
412
- type="checkbox"
436
+ <div className="inline-flex items-center gap-2 text-sm">
437
+ <Checkbox
438
+ id="entities-encryption-active"
413
439
  checked={isActive}
414
- onChange={(event) => setIsActive(event.target.checked)}
440
+ onCheckedChange={(checked) => setIsActive(checked === true)}
415
441
  />
416
- {t('entities.encryption.active', 'Encryption enabled for this entity')}
417
- </label>
442
+ <label htmlFor="entities-encryption-active">
443
+ {t('entities.encryption.active', 'Encryption enabled for this entity')}
444
+ </label>
445
+ </div>
418
446
  </div>
419
447
  </div>
420
448
  <div className="rounded-lg border bg-background/80 p-4">
@@ -425,7 +453,7 @@ export function EncryptionManager() {
425
453
  {t('entities.encryption.fields.subtitle', 'List the attributes that should be encrypted with the tenant key.')}
426
454
  </p>
427
455
  </div>
428
- <Button variant="outline" size="sm" onClick={addField}>
456
+ <Button type="button" variant="outline" size="sm" onClick={addField}>
429
457
  <Plus className="mr-2 h-4 w-4" />
430
458
  {t('entities.encryption.actions.add', 'Add field')}
431
459
  </Button>
@@ -434,6 +462,7 @@ export function EncryptionManager() {
434
462
  </div>
435
463
  <div className="flex justify-end">
436
464
  <Button
465
+ type="button"
437
466
  onClick={() => mutation.mutate()}
438
467
  disabled={mutation.isPending || loadingEntities || !!entitiesError || !selectedEntityId}
439
468
  >
@@ -163,6 +163,16 @@ export async function runExtractionWithConfiguredProvider(input: {
163
163
  model = factoryResolution.model
164
164
  modelWithProvider = `${factoryResolution.providerId}/${factoryResolution.modelId}`
165
165
  } else {
166
+ // BC: Legacy OPENCODE_PROVIDER / OPENCODE_MODEL path. This branch only
167
+ // runs when createModelFactory throws AiModelFactoryError('no_provider_configured'),
168
+ // meaning none of the new-style provider env vars (ANTHROPIC_API_KEY,
169
+ // OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OM_AI_PROVIDER, ...) is
170
+ // set. The OPENCODE_* envs are deliberately kept as BC fallbacks instead
171
+ // of being treated as canonical OM_AI_* values because they are scoped to
172
+ // the OpenCode Code Mode stack (spec
173
+ // 2026-04-27-ai-agents-provider-model-baseurl-overrides, R1 mitigation).
174
+ // Keeping this fallback alive preserves backward compatibility for existing
175
+ // inbox_ops deployments that have not yet migrated to the new env vars.
166
176
  const providerId = resolveExtractionProviderId()
167
177
  const apiKey = requireOpenCodeProviderApiKey(providerId)
168
178
  const modelConfig = resolveOpenCodeModel(providerId, {
@@ -35,7 +35,9 @@ import {
35
35
  type IntegrationDetailBuiltInTab,
36
36
  } from '@open-mercato/shared/modules/integrations/types'
37
37
  import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
38
- import { Activity, AlertTriangle, Bell, Calendar, CheckCircle2, ChevronDown, ChevronRight, CreditCard, FileText, HardDrive, Key, MessageSquare, RefreshCw, Settings, Truck, Webhook, XCircle, Zap } from 'lucide-react'
38
+ import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
39
+ import { Activity, AlertTriangle, Bell, Calendar, CheckCircle2, CreditCard, FileText, FileX, HardDrive, Key, MessageSquare, RefreshCw, Settings, Truck, Webhook, XCircle, Zap } from 'lucide-react'
40
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
39
41
  import { IntegrationScheduleTab } from '../../../../data_sync/components/IntegrationScheduleTab'
40
42
  import {
41
43
  buildIntegrationDetailInjectedTabs,
@@ -129,12 +131,6 @@ type HealthCheckResponse = {
129
131
  latencyMs: number | null
130
132
  }
131
133
 
132
- const LOG_LEVEL_STYLES: Record<string, string> = {
133
- info: 'bg-blue-100 text-blue-800',
134
- warn: 'bg-yellow-100 text-yellow-800',
135
- error: 'bg-red-100 text-red-800',
136
- }
137
-
138
134
  const HEALTH_STATUS_STYLES: Record<string, string> = {
139
135
  healthy: 'bg-green-100 text-green-800',
140
136
  degraded: 'bg-yellow-100 text-yellow-800',
@@ -337,7 +333,6 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
337
333
  const [logs, setLogs] = React.useState<LogEntry[]>([])
338
334
  const [logLevel, setLogLevel] = React.useState<string>('')
339
335
  const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)
340
- const [expandedLogId, setExpandedLogId] = React.useState<string | null>(null)
341
336
 
342
337
  const [isCheckingHealth, setIsCheckingHealth] = React.useState(false)
343
338
  const [isTogglingState, setIsTogglingState] = React.useState(false)
@@ -522,9 +517,6 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
522
517
  React.useEffect(() => { void loadDetail() }, [loadDetail])
523
518
  React.useEffect(() => { void loadCredentials() }, [loadCredentials])
524
519
  React.useEffect(() => { void loadLogs() }, [loadLogs])
525
- React.useEffect(() => {
526
- setExpandedLogId((current) => (current && logs.some((log) => log.id === current) ? current : null))
527
- }, [logs])
528
520
 
529
521
  const handleToggleState = React.useCallback(async (enabled: boolean) => {
530
522
  const currentIntegrationId = resolveCurrentIntegrationId()
@@ -1210,144 +1202,107 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1210
1202
  </div>
1211
1203
  {isLoadingLogs ? (
1212
1204
  <div className="flex justify-center py-8"><Spinner /></div>
1213
- ) : logs.length === 0 ? (
1214
- <p className="py-4 text-sm text-muted-foreground">{t('integrations.detail.logs.empty')}</p>
1215
1205
  ) : (
1216
- <div className="rounded-lg border">
1217
- <table className="w-full text-sm">
1218
- <thead>
1219
- <tr className="border-b bg-muted/50">
1220
- <th className="px-4 py-2 text-left font-medium">{t('integrations.detail.logs.columns.time')}</th>
1221
- <th className="px-4 py-2 text-left font-medium">{t('integrations.detail.logs.columns.level')}</th>
1222
- <th className="px-4 py-2 text-left font-medium">{t('integrations.detail.logs.columns.message')}</th>
1223
- </tr>
1224
- </thead>
1225
- <tbody>
1226
- {logs.map((log) => {
1227
- const isExpanded = expandedLogId === log.id
1228
- const metadataEntries = [
1229
- ['Time', new Date(log.createdAt).toLocaleString()],
1230
- ['Level', log.level],
1231
- ['Code', log.code ?? null],
1232
- ['Run ID', log.runId ?? null],
1233
- ['Entity Type', log.scopeEntityType ?? null],
1234
- ['Entity ID', log.scopeEntityId ?? null],
1235
- ].filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim().length > 0)
1236
- const { inlineEntries, nestedEntries } = splitLogPayload(log.payload)
1237
-
1238
- return (
1239
- <React.Fragment key={log.id}>
1240
- <tr className="border-b last:border-0">
1241
- <td className="whitespace-nowrap px-4 py-2 text-muted-foreground">
1242
- {new Date(log.createdAt).toLocaleString()}
1243
- </td>
1244
- <td className="px-4 py-2">
1245
- <Badge variant="secondary" className={LOG_LEVEL_STYLES[log.level] ?? ''}>
1246
- {log.level}
1247
- </Badge>
1248
- </td>
1249
- <td className="px-4 py-2">
1250
- <Button
1251
- type="button"
1252
- variant="ghost"
1253
- size="sm"
1254
- className="h-auto w-full justify-start gap-2 px-0 py-0 text-left hover:bg-transparent"
1255
- onClick={() => setExpandedLogId((current) => (current === log.id ? null : log.id))}
1256
- >
1257
- {isExpanded ? <ChevronDown className="h-4 w-4 shrink-0" /> : <ChevronRight className="h-4 w-4 shrink-0" />}
1258
- <span className="truncate">{log.message}</span>
1259
- </Button>
1260
- </td>
1261
- </tr>
1262
- {isExpanded ? (
1263
- <tr className="border-b bg-muted/30 last:border-0">
1264
- <td colSpan={3} className="px-4 py-4">
1265
- <div className="space-y-4 rounded-lg border bg-card p-4">
1266
- <div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
1267
- <div className="space-y-4">
1268
- <section className="space-y-3">
1269
- <div>
1270
- <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
1271
- {t('integrations.detail.logs.details.summary', 'Summary')}
1272
- </p>
1273
- <p className="mt-1 text-sm font-medium">{log.message}</p>
1274
- </div>
1275
- {metadataEntries.length > 0 ? (
1276
- <dl className="grid gap-3 sm:grid-cols-2">
1277
- {metadataEntries.map(([label, value]) => (
1278
- <div key={label} className="rounded-md border bg-muted/30 px-3 py-2">
1279
- <dt className="text-overline font-medium uppercase tracking-wide text-muted-foreground">
1280
- {label}
1281
- </dt>
1282
- <dd className="mt-1 break-all text-sm">{value}</dd>
1283
- </div>
1284
- ))}
1285
- </dl>
1286
- ) : null}
1287
- </section>
1288
-
1289
- {inlineEntries.length > 0 ? (
1290
- <section className="space-y-3">
1291
- <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
1292
- {t('integrations.detail.logs.details.fields', 'Fields')}
1293
- </p>
1294
- <dl className="grid gap-3 sm:grid-cols-2">
1295
- {inlineEntries.map(([key, value]) => (
1296
- <div key={key} className="rounded-md border bg-muted/30 px-3 py-2">
1297
- <dt className="text-overline font-medium uppercase tracking-wide text-muted-foreground">
1298
- {formatLogDetailLabel(key)}
1299
- </dt>
1300
- <dd className="mt-1 break-words text-sm">
1301
- {formatLogPrimitiveValue(value)}
1302
- </dd>
1303
- </div>
1304
- ))}
1305
- </dl>
1306
- </section>
1307
- ) : null}
1308
- </div>
1309
-
1310
- <div className="space-y-3">
1311
- {nestedEntries.map(([key, value]) => (
1312
- <JsonDisplay
1313
- key={key}
1314
- data={value}
1315
- title={formatLogDetailLabel(key)}
1316
- defaultExpanded
1317
- maxInitialDepth={1}
1318
- theme="dark"
1319
- maxHeight="16rem"
1320
- className="p-4"
1321
- />
1322
- ))}
1323
- {log.payload && nestedEntries.length === 0 ? (
1324
- <JsonDisplay
1325
- data={log.payload}
1326
- title={t('integrations.detail.logs.details.payload', 'Payload')}
1327
- defaultExpanded
1328
- maxInitialDepth={1}
1329
- theme="dark"
1330
- maxHeight="16rem"
1331
- className="p-4"
1332
- />
1333
- ) : null}
1334
- {!log.payload ? (
1335
- <div className="rounded-lg border border-dashed px-4 py-6 text-sm text-muted-foreground">
1336
- {t('integrations.detail.logs.details.noPayload', 'No structured payload was stored for this log entry.')}
1337
- </div>
1338
- ) : null}
1339
- </div>
1206
+ <LogList
1207
+ entries={logs.map<LogListEntry>((log) => {
1208
+ const metadataEntries = [
1209
+ ['Time', new Date(log.createdAt).toLocaleString()],
1210
+ ['Level', log.level],
1211
+ ['Code', log.code ?? null],
1212
+ ['Run ID', log.runId ?? null],
1213
+ ['Entity Type', log.scopeEntityType ?? null],
1214
+ ['Entity ID', log.scopeEntityId ?? null],
1215
+ ].filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim().length > 0)
1216
+ const { inlineEntries, nestedEntries } = splitLogPayload(log.payload)
1217
+
1218
+ return {
1219
+ id: log.id,
1220
+ time: new Date(log.createdAt).toLocaleString(),
1221
+ level: log.level,
1222
+ message: log.message,
1223
+ body: (
1224
+ <div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
1225
+ <div className="space-y-4">
1226
+ <section className="space-y-3">
1227
+ <div>
1228
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
1229
+ {t('integrations.detail.logs.details.summary', 'Summary')}
1230
+ </p>
1231
+ <p className="mt-1 text-sm font-medium">{log.message}</p>
1232
+ </div>
1233
+ {metadataEntries.length > 0 ? (
1234
+ <dl className="grid gap-3 sm:grid-cols-2">
1235
+ {metadataEntries.map(([label, value]) => (
1236
+ <div key={label} className="rounded-md border bg-muted/30 px-3 py-2">
1237
+ <dt className="text-overline font-medium uppercase tracking-wide text-muted-foreground">
1238
+ {label}
1239
+ </dt>
1240
+ <dd className="mt-1 break-all text-sm">{value}</dd>
1241
+ </div>
1242
+ ))}
1243
+ </dl>
1244
+ ) : null}
1245
+ </section>
1246
+
1247
+ {inlineEntries.length > 0 ? (
1248
+ <section className="space-y-3">
1249
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
1250
+ {t('integrations.detail.logs.details.fields', 'Fields')}
1251
+ </p>
1252
+ <dl className="grid gap-3 sm:grid-cols-2">
1253
+ {inlineEntries.map(([key, value]) => (
1254
+ <div key={key} className="rounded-md border bg-muted/30 px-3 py-2">
1255
+ <dt className="text-overline font-medium uppercase tracking-wide text-muted-foreground">
1256
+ {formatLogDetailLabel(key)}
1257
+ </dt>
1258
+ <dd className="mt-1 break-words text-sm">
1259
+ {formatLogPrimitiveValue(value)}
1260
+ </dd>
1340
1261
  </div>
1341
- </div>
1342
- </td>
1343
- </tr>
1262
+ ))}
1263
+ </dl>
1264
+ </section>
1344
1265
  ) : null}
1345
- </React.Fragment>
1346
- )
1347
- })}
1348
- </tbody>
1349
- </table>
1350
- </div>
1266
+ </div>
1267
+
1268
+ <div className="space-y-3">
1269
+ {nestedEntries.map(([key, value]) => (
1270
+ <JsonDisplay
1271
+ key={key}
1272
+ data={value}
1273
+ title={formatLogDetailLabel(key)}
1274
+ defaultExpanded
1275
+ maxInitialDepth={1}
1276
+ theme="dark"
1277
+ maxHeight="16rem"
1278
+ className="p-4"
1279
+ />
1280
+ ))}
1281
+ {log.payload && nestedEntries.length === 0 ? (
1282
+ <JsonDisplay
1283
+ data={log.payload}
1284
+ title={t('integrations.detail.logs.details.payload', 'Payload')}
1285
+ defaultExpanded
1286
+ maxInitialDepth={1}
1287
+ theme="dark"
1288
+ maxHeight="16rem"
1289
+ className="p-4"
1290
+ />
1291
+ ) : null}
1292
+ {!log.payload ? (
1293
+ <EmptyState
1294
+ size="sm"
1295
+ icon={<FileX className="h-8 w-8" aria-hidden="true" />}
1296
+ title={t('integrations.detail.logs.details.noPayload', 'No structured payload was stored for this log entry.')}
1297
+ />
1298
+ ) : null}
1299
+ </div>
1300
+ </div>
1301
+ ),
1302
+ }
1303
+ })}
1304
+ emptyMessage={t('integrations.detail.logs.empty')}
1305
+ />
1351
1306
  )}
1352
1307
  </TabsContent>
1353
1308
  ) : null}