@open-mercato/core 0.4.7-develop-78d7541539 → 0.4.7-develop-74069040de

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 (187) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/modules/catalog/api/bulk-delete/route.js +86 -0
  3. package/dist/modules/catalog/api/bulk-delete/route.js.map +7 -0
  4. package/dist/modules/catalog/api/prices/route.js +39 -6
  5. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  6. package/dist/modules/catalog/api/products/route.js +6 -11
  7. package/dist/modules/catalog/api/products/route.js.map +2 -2
  8. package/dist/modules/catalog/commands/products.js +2 -0
  9. package/dist/modules/catalog/commands/products.js.map +2 -2
  10. package/dist/modules/catalog/components/products/ProductsDataTable.js +9 -1
  11. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  12. package/dist/modules/catalog/lib/bulkDelete.js +70 -0
  13. package/dist/modules/catalog/lib/bulkDelete.js.map +7 -0
  14. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js +185 -0
  15. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js.map +7 -0
  16. package/dist/modules/catalog/widgets/injection-table.js +9 -1
  17. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  18. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js +40 -0
  19. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js.map +7 -0
  20. package/dist/modules/data_sync/api/options.js +52 -0
  21. package/dist/modules/data_sync/api/options.js.map +7 -0
  22. package/dist/modules/data_sync/api/run.js +30 -35
  23. package/dist/modules/data_sync/api/run.js.map +2 -2
  24. package/dist/modules/data_sync/api/runs/[id]/cancel.js +2 -2
  25. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  26. package/dist/modules/data_sync/api/runs/[id]/retry.js +15 -30
  27. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  28. package/dist/modules/data_sync/api/schedules/[id]/route.js +109 -0
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
  30. package/dist/modules/data_sync/api/schedules/route.js +72 -0
  31. package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
  32. package/dist/modules/data_sync/api/schedules/serialize.js +21 -0
  33. package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
  34. package/dist/modules/data_sync/backend/data-sync/page.js +656 -47
  35. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  36. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +116 -34
  37. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  38. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +394 -0
  39. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
  40. package/dist/modules/data_sync/data/validators.js +32 -0
  41. package/dist/modules/data_sync/data/validators.js.map +2 -2
  42. package/dist/modules/data_sync/di.js +2 -0
  43. package/dist/modules/data_sync/di.js.map +2 -2
  44. package/dist/modules/data_sync/lib/id-mapping.js +24 -2
  45. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  46. package/dist/modules/data_sync/lib/start-run.js +57 -0
  47. package/dist/modules/data_sync/lib/start-run.js.map +7 -0
  48. package/dist/modules/data_sync/lib/sync-engine.js +93 -4
  49. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  50. package/dist/modules/data_sync/lib/sync-run-service.js +5 -1
  51. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  52. package/dist/modules/data_sync/lib/sync-schedule-service.js +138 -0
  53. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
  54. package/dist/modules/data_sync/workers/sync-export.js +28 -2
  55. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  56. package/dist/modules/data_sync/workers/sync-import.js +28 -2
  57. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  58. package/dist/modules/data_sync/workers/sync-scheduled.js +5 -0
  59. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  60. package/dist/modules/entities/api/definitions.js +5 -2
  61. package/dist/modules/entities/api/definitions.js.map +2 -2
  62. package/dist/modules/entities/lib/field-definitions.js +3 -1
  63. package/dist/modules/entities/lib/field-definitions.js.map +2 -2
  64. package/dist/modules/integrations/api/[id]/route.js +14 -15
  65. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  66. package/dist/modules/integrations/api/route.js +3 -3
  67. package/dist/modules/integrations/api/route.js.map +2 -2
  68. package/dist/modules/integrations/backend/integrations/[id]/page.js +148 -33
  69. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  70. package/dist/modules/integrations/lib/state-service.js +15 -1
  71. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  72. package/dist/modules/messages/api/[id]/route.js +24 -22
  73. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  74. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  75. package/dist/modules/progress/api/active/route.js +3 -1
  76. package/dist/modules/progress/api/active/route.js.map +2 -2
  77. package/dist/modules/progress/api/jobs/[id]/route.js +1 -1
  78. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  79. package/dist/modules/progress/api/jobs/route.js +1 -1
  80. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  81. package/dist/modules/progress/lib/events.js.map +1 -1
  82. package/dist/modules/progress/lib/progressService.js.map +2 -2
  83. package/dist/modules/progress/lib/progressServiceImpl.js +42 -1
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/lib/document.js +35 -1
  86. package/dist/modules/query_index/lib/document.js.map +2 -2
  87. package/dist/modules/query_index/lib/engine.js +91 -4
  88. package/dist/modules/query_index/lib/engine.js.map +2 -2
  89. package/dist/modules/query_index/lib/indexer.js +2 -0
  90. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  91. package/dist/modules/sales/api/adjustment-kinds/route.js +3 -9
  92. package/dist/modules/sales/api/adjustment-kinds/route.js.map +2 -2
  93. package/dist/modules/sales/api/channels/route.js +3 -10
  94. package/dist/modules/sales/api/channels/route.js.map +2 -2
  95. package/dist/modules/sales/api/delivery-windows/route.js +3 -10
  96. package/dist/modules/sales/api/delivery-windows/route.js.map +2 -2
  97. package/dist/modules/sales/api/payment-methods/route.js +3 -11
  98. package/dist/modules/sales/api/payment-methods/route.js.map +2 -2
  99. package/dist/modules/sales/api/price-kinds/route.js +3 -5
  100. package/dist/modules/sales/api/price-kinds/route.js.map +2 -2
  101. package/dist/modules/sales/api/shipping-methods/route.js +3 -11
  102. package/dist/modules/sales/api/shipping-methods/route.js.map +2 -2
  103. package/dist/modules/sales/api/tags/route.js +3 -9
  104. package/dist/modules/sales/api/tags/route.js.map +2 -2
  105. package/dist/modules/sales/api/tax-rates/route.js +3 -13
  106. package/dist/modules/sales/api/tax-rates/route.js.map +2 -2
  107. package/dist/modules/sales/api/utils.js +9 -0
  108. package/dist/modules/sales/api/utils.js.map +2 -2
  109. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +3 -9
  110. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +2 -2
  111. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -2
  112. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  113. package/dist/modules/workflows/api/definitions/route.js +4 -3
  114. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  115. package/dist/modules/workflows/api/definitions/serialize.js +25 -0
  116. package/dist/modules/workflows/api/definitions/serialize.js.map +7 -0
  117. package/package.json +3 -3
  118. package/src/modules/catalog/api/bulk-delete/route.ts +93 -0
  119. package/src/modules/catalog/api/prices/route.ts +53 -6
  120. package/src/modules/catalog/api/products/route.ts +6 -11
  121. package/src/modules/catalog/commands/products.ts +2 -0
  122. package/src/modules/catalog/components/products/ProductsDataTable.tsx +8 -0
  123. package/src/modules/catalog/i18n/de.json +10 -0
  124. package/src/modules/catalog/i18n/en.json +10 -0
  125. package/src/modules/catalog/i18n/es.json +10 -0
  126. package/src/modules/catalog/i18n/pl.json +10 -0
  127. package/src/modules/catalog/lib/bulkDelete.ts +106 -0
  128. package/src/modules/catalog/widgets/injection/product-bulk-delete/widget.ts +242 -0
  129. package/src/modules/catalog/widgets/injection-table.ts +8 -0
  130. package/src/modules/catalog/workers/catalog-product-bulk-delete.ts +48 -0
  131. package/src/modules/data_sync/AGENTS.md +11 -3
  132. package/src/modules/data_sync/api/options.ts +58 -0
  133. package/src/modules/data_sync/api/run.ts +34 -36
  134. package/src/modules/data_sync/api/runs/[id]/cancel.ts +2 -2
  135. package/src/modules/data_sync/api/runs/[id]/retry.ts +14 -31
  136. package/src/modules/data_sync/api/schedules/[id]/route.ts +130 -0
  137. package/src/modules/data_sync/api/schedules/route.ts +77 -0
  138. package/src/modules/data_sync/api/schedules/serialize.ts +31 -0
  139. package/src/modules/data_sync/backend/data-sync/page.tsx +756 -2
  140. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +179 -53
  141. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +512 -0
  142. package/src/modules/data_sync/data/validators.ts +35 -0
  143. package/src/modules/data_sync/di.ts +6 -0
  144. package/src/modules/data_sync/i18n/de.json +72 -0
  145. package/src/modules/data_sync/i18n/en.json +72 -0
  146. package/src/modules/data_sync/i18n/es.json +72 -0
  147. package/src/modules/data_sync/i18n/pl.json +72 -0
  148. package/src/modules/data_sync/lib/adapter.ts +4 -1
  149. package/src/modules/data_sync/lib/id-mapping.ts +32 -2
  150. package/src/modules/data_sync/lib/start-run.ts +90 -0
  151. package/src/modules/data_sync/lib/sync-engine.ts +111 -4
  152. package/src/modules/data_sync/lib/sync-run-service.ts +5 -1
  153. package/src/modules/data_sync/lib/sync-schedule-service.ts +207 -0
  154. package/src/modules/data_sync/workers/sync-export.ts +33 -2
  155. package/src/modules/data_sync/workers/sync-import.ts +33 -2
  156. package/src/modules/data_sync/workers/sync-scheduled.ts +7 -0
  157. package/src/modules/entities/api/definitions.ts +12 -2
  158. package/src/modules/entities/lib/field-definitions.ts +2 -0
  159. package/src/modules/integrations/AGENTS.md +16 -3
  160. package/src/modules/integrations/api/[id]/route.ts +14 -15
  161. package/src/modules/integrations/api/route.ts +3 -3
  162. package/src/modules/integrations/backend/integrations/[id]/page.tsx +176 -54
  163. package/src/modules/integrations/lib/state-service.ts +25 -1
  164. package/src/modules/messages/api/[id]/route.ts +25 -22
  165. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +3 -3
  166. package/src/modules/progress/api/active/route.ts +4 -1
  167. package/src/modules/progress/api/jobs/[id]/route.ts +1 -1
  168. package/src/modules/progress/api/jobs/route.ts +1 -1
  169. package/src/modules/progress/lib/events.ts +6 -0
  170. package/src/modules/progress/lib/progressService.ts +1 -0
  171. package/src/modules/progress/lib/progressServiceImpl.ts +47 -1
  172. package/src/modules/query_index/lib/document.ts +52 -1
  173. package/src/modules/query_index/lib/engine.ts +104 -4
  174. package/src/modules/query_index/lib/indexer.ts +2 -0
  175. package/src/modules/sales/api/adjustment-kinds/route.ts +3 -9
  176. package/src/modules/sales/api/channels/route.ts +3 -10
  177. package/src/modules/sales/api/delivery-windows/route.ts +3 -10
  178. package/src/modules/sales/api/payment-methods/route.ts +3 -11
  179. package/src/modules/sales/api/price-kinds/route.ts +3 -5
  180. package/src/modules/sales/api/shipping-methods/route.ts +3 -11
  181. package/src/modules/sales/api/tags/route.ts +3 -9
  182. package/src/modules/sales/api/tax-rates/route.ts +3 -13
  183. package/src/modules/sales/api/utils.ts +9 -0
  184. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +3 -9
  185. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -2
  186. package/src/modules/workflows/api/definitions/route.ts +4 -3
  187. package/src/modules/workflows/api/definitions/serialize.ts +23 -0
@@ -20,18 +20,21 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
20
20
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
21
21
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
22
22
  import { useT } from '@open-mercato/shared/lib/i18n/context'
23
+ import { cn } from '@open-mercato/shared/lib/utils'
23
24
  import { LEGACY_INTEGRATION_DETAIL_TABS_SPOT_ID, type CredentialFieldType, type IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
24
25
  import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
25
- import { Bell, ChevronDown, ChevronRight, CreditCard, HardDrive, MessageSquare, RefreshCw, Truck, Webhook, Zap } from 'lucide-react'
26
+ import { Activity, AlertTriangle, Bell, Calendar, CheckCircle2, ChevronDown, ChevronRight, CreditCard, FileText, HardDrive, Key, MessageSquare, RefreshCw, Settings, Truck, Webhook, XCircle, Zap } from 'lucide-react'
27
+ import { IntegrationScheduleTab } from '../../../../data_sync/components/IntegrationScheduleTab'
26
28
  import {
27
29
  buildIntegrationDetailInjectedTabs,
28
30
  filterIntegrationDetailWidgetsByKind,
31
+ type IntegrationDetailInjectedTab,
29
32
  resolveIntegrationDetailWidgetSpotId,
30
33
  resolveRequestedIntegrationDetailTab,
31
34
  } from '../detail-page-widgets'
32
35
 
33
36
  type CredentialField = IntegrationCredentialField
34
- type BuiltInIntegrationDetailTab = 'credentials' | 'version' | 'health' | 'logs'
37
+ type BuiltInIntegrationDetailTab = 'credentials' | 'version' | 'health' | 'logs' | 'data-sync-schedule'
35
38
  type IntegrationDetailTab = BuiltInIntegrationDetailTab | string
36
39
 
37
40
  const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
@@ -55,6 +58,7 @@ type IntegrationDetail = {
55
58
  description?: string
56
59
  category?: string
57
60
  hub?: string
61
+ providerKey?: string | null
58
62
  bundleId?: string
59
63
  docsUrl?: string
60
64
  apiVersions?: ApiVersion[]
@@ -111,6 +115,12 @@ const HEALTH_STATUS_STYLES: Record<string, string> = {
111
115
  unhealthy: 'bg-red-100 text-red-800',
112
116
  }
113
117
 
118
+ const HEALTH_STATUS_ICONS: Record<string, React.ElementType> = {
119
+ healthy: CheckCircle2,
120
+ degraded: AlertTriangle,
121
+ unhealthy: XCircle,
122
+ }
123
+
114
124
  const CATEGORY_ICONS: Record<string, React.ElementType> = {
115
125
  payment: CreditCard,
116
126
  shipping: Truck,
@@ -230,6 +240,10 @@ function formatLogPrimitiveValue(value: string | number | boolean | null): strin
230
240
  return String(value)
231
241
  }
232
242
 
243
+ function isAkeneoSettingsTab(tab: IntegrationDetailInjectedTab): boolean {
244
+ return tab.id.includes('sync_akeneo') || tab.label.toLowerCase().includes('akeneo')
245
+ }
246
+
233
247
  function splitLogPayload(payload: Record<string, unknown> | null | undefined) {
234
248
  if (!payload) {
235
249
  return {
@@ -419,9 +433,17 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
419
433
  ),
420
434
  [detailWidgets, t],
421
435
  )
436
+ const hasDataSyncScheduleTab = Boolean(
437
+ detail?.integration.hub === 'data_sync'
438
+ && detail?.integration.providerKey
439
+ && detail.integration.providerKey.trim().length > 0,
440
+ )
422
441
  const customTabIds = React.useMemo(
423
- () => injectedTabs.map((tab) => tab.id),
424
- [injectedTabs],
442
+ () => [
443
+ ...(hasDataSyncScheduleTab ? ['data-sync-schedule'] : []),
444
+ ...injectedTabs.map((tab) => tab.id),
445
+ ],
446
+ [hasDataSyncScheduleTab, injectedTabs],
425
447
  )
426
448
  const runMutationWithContext = React.useCallback(
427
449
  async <T,>({
@@ -703,6 +725,25 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
703
725
  const resolvedIntegration = detail.integration
704
726
  const resolvedState = detail.state
705
727
  const CategoryIcon = resolvedIntegration.category ? CATEGORY_ICONS[resolvedIntegration.category] : null
728
+ const HealthStatusIcon = resolvedState.lastHealthStatus ? HEALTH_STATUS_ICONS[resolvedState.lastHealthStatus] : null
729
+ const prioritizedInjectedTabs = resolvedIntegration.id === 'sync_akeneo'
730
+ ? [...injectedTabs].sort((left, right) => {
731
+ const leftPriority = isAkeneoSettingsTab(left) ? 1 : 0
732
+ const rightPriority = isAkeneoSettingsTab(right) ? 1 : 0
733
+ if (leftPriority !== rightPriority) return rightPriority - leftPriority
734
+ return 0
735
+ })
736
+ : injectedTabs
737
+ const leadingInjectedTab = resolvedIntegration.id === 'sync_akeneo'
738
+ ? prioritizedInjectedTabs.find(isAkeneoSettingsTab) ?? null
739
+ : null
740
+ const trailingInjectedTabs = leadingInjectedTab
741
+ ? prioritizedInjectedTabs.filter((tab) => tab.id !== leadingInjectedTab.id)
742
+ : prioritizedInjectedTabs
743
+ const StateIcon = resolvedState.isEnabled ? CheckCircle2 : XCircle
744
+ const stateBadgeClass = resolvedState.isEnabled
745
+ ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
746
+ : 'border-zinc-500/30 bg-zinc-500/10 text-zinc-300'
706
747
 
707
748
  const showCredentialActions = activeTab === 'credentials' && credentialFormFields.length > 0
708
749
 
@@ -749,15 +790,16 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
749
790
 
750
791
  <section className="rounded-lg border bg-card p-4">
751
792
  <div className="flex items-center justify-between gap-4">
752
- <div className="space-y-1">
793
+ <div className="space-y-2">
753
794
  <p className="text-[11px] uppercase tracking-wide text-muted-foreground">
754
795
  {t('integrations.detail.state.label', 'State')}
755
796
  </p>
756
- <p className="text-sm font-medium">
797
+ <Badge variant="outline" className={cn('gap-1.5 rounded-full px-3 py-1 text-xs font-medium', stateBadgeClass)}>
798
+ <StateIcon className="h-3.5 w-3.5" />
757
799
  {resolvedState.isEnabled
758
800
  ? t('integrations.detail.state.enabled', 'Enabled')
759
801
  : t('integrations.detail.state.disabled', 'Disabled')}
760
- </p>
802
+ </Badge>
761
803
  </div>
762
804
  <Switch
763
805
  checked={resolvedState.isEnabled}
@@ -810,14 +852,79 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
810
852
  </section>
811
853
  ) : null}
812
854
 
813
- <Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
814
- <TabsList>
815
- <TabsTrigger value="credentials">{t('integrations.detail.tabs.credentials')}</TabsTrigger>
816
- {hasVersions ? <TabsTrigger value="version">{t('integrations.detail.tabs.version')}</TabsTrigger> : null}
817
- <TabsTrigger value="health">{t('integrations.detail.tabs.health')}</TabsTrigger>
818
- <TabsTrigger value="logs">{t('integrations.detail.tabs.logs')}</TabsTrigger>
819
- {injectedTabs.map((tab) => (
820
- <TabsTrigger key={tab.id} value={tab.id}>{tab.label}</TabsTrigger>
855
+ <Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-5">
856
+ <TabsList className="h-auto w-full justify-start overflow-x-auto rounded-none border-b border-border bg-transparent p-0">
857
+ <TabsTrigger
858
+ value="credentials"
859
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
860
+ >
861
+ <span className="inline-flex items-center gap-2">
862
+ <Key className="h-4 w-4" />
863
+ <span>{t('integrations.detail.tabs.credentials')}</span>
864
+ </span>
865
+ </TabsTrigger>
866
+ {leadingInjectedTab ? (
867
+ <TabsTrigger
868
+ value={leadingInjectedTab.id}
869
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
870
+ >
871
+ <span className="inline-flex items-center gap-2">
872
+ <Settings className="h-4 w-4" />
873
+ <span>{leadingInjectedTab.label}</span>
874
+ </span>
875
+ </TabsTrigger>
876
+ ) : null}
877
+ {hasVersions ? (
878
+ <TabsTrigger
879
+ value="version"
880
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
881
+ >
882
+ <span className="inline-flex items-center gap-2">
883
+ <RefreshCw className="h-4 w-4" />
884
+ <span>{t('integrations.detail.tabs.version')}</span>
885
+ </span>
886
+ </TabsTrigger>
887
+ ) : null}
888
+ {hasDataSyncScheduleTab ? (
889
+ <TabsTrigger
890
+ value="data-sync-schedule"
891
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
892
+ >
893
+ <span className="inline-flex items-center gap-2">
894
+ <Calendar className="h-4 w-4" />
895
+ <span>{t('data_sync.integrationTab.title', 'Sync schedules')}</span>
896
+ </span>
897
+ </TabsTrigger>
898
+ ) : null}
899
+ <TabsTrigger
900
+ value="health"
901
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
902
+ >
903
+ <span className="inline-flex items-center gap-2">
904
+ <Activity className="h-4 w-4" />
905
+ <span>{t('integrations.detail.tabs.health')}</span>
906
+ </span>
907
+ </TabsTrigger>
908
+ <TabsTrigger
909
+ value="logs"
910
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
911
+ >
912
+ <span className="inline-flex items-center gap-2">
913
+ <FileText className="h-4 w-4" />
914
+ <span>{t('integrations.detail.tabs.logs')}</span>
915
+ </span>
916
+ </TabsTrigger>
917
+ {trailingInjectedTabs.map((tab) => (
918
+ <TabsTrigger
919
+ key={tab.id}
920
+ value={tab.id}
921
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
922
+ >
923
+ <span className="inline-flex items-center gap-2">
924
+ <Settings className="h-4 w-4" />
925
+ <span>{tab.label}</span>
926
+ </span>
927
+ </TabsTrigger>
821
928
  ))}
822
929
  </TabsList>
823
930
 
@@ -887,9 +994,19 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
887
994
  </TabsContent>
888
995
  ) : null}
889
996
 
997
+ {hasDataSyncScheduleTab ? (
998
+ <TabsContent value="data-sync-schedule" className="mt-0">
999
+ <IntegrationScheduleTab
1000
+ integrationId={resolvedIntegration.id}
1001
+ hasCredentials={detail.hasCredentials}
1002
+ isEnabled={resolvedState.isEnabled}
1003
+ />
1004
+ </TabsContent>
1005
+ ) : null}
1006
+
890
1007
  <TabsContent value="health" className="mt-0 space-y-4">
891
- <Card>
892
- <CardHeader>
1008
+ <Card className="gap-4 py-4">
1009
+ <CardHeader className="px-5">
893
1010
  <div className="flex items-center justify-between">
894
1011
  <CardTitle>{t('integrations.detail.health.title')}</CardTitle>
895
1012
  <Button
@@ -904,51 +1021,56 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
904
1021
  </Button>
905
1022
  </div>
906
1023
  </CardHeader>
907
- <CardContent className="space-y-3">
908
- <div className="flex items-center gap-3">
909
- <span className="text-sm font-medium">{t('integrations.detail.health.title')}:</span>
1024
+ <CardContent className="space-y-3 px-5">
1025
+ <div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/20 px-4 py-3">
910
1026
  {resolvedState.lastHealthStatus ? (
911
- <Badge className={HEALTH_STATUS_STYLES[resolvedState.lastHealthStatus] ?? ''}>
1027
+ <Badge className={`gap-1.5 ${HEALTH_STATUS_STYLES[resolvedState.lastHealthStatus] ?? ''}`}>
1028
+ {HealthStatusIcon ? <HealthStatusIcon className="h-3.5 w-3.5" /> : null}
912
1029
  {t(`integrations.detail.health.${resolvedState.lastHealthStatus}`)}
913
1030
  </Badge>
914
1031
  ) : (
915
- <span className="text-sm text-muted-foreground">
916
- {t('integrations.detail.health.unknown')}
917
- </span>
1032
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
1033
+ <AlertTriangle className="h-4 w-4" />
1034
+ <span>{t('integrations.detail.health.unknown')}</span>
1035
+ </div>
918
1036
  )}
1037
+ {healthStatusDescription ? (
1038
+ <p className="min-w-0 flex-1 text-sm text-muted-foreground">{healthStatusDescription}</p>
1039
+ ) : null}
1040
+ <p className="text-xs text-muted-foreground md:ml-auto">
1041
+ {resolvedState.lastHealthCheckedAt
1042
+ ? t('integrations.detail.health.lastChecked', { date: new Date(resolvedState.lastHealthCheckedAt).toLocaleString() })
1043
+ : t('integrations.detail.health.neverChecked')
1044
+ }
1045
+ </p>
919
1046
  </div>
920
- {healthStatusDescription ? (
921
- <p className="text-sm text-muted-foreground">{healthStatusDescription}</p>
922
- ) : null}
923
- {healthMessage ? (
924
- <div className="rounded-lg border bg-muted/30 p-3">
925
- <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
926
- {t('integrations.detail.health.lastResult', 'Last result')}
927
- </p>
928
- <p className="mt-1 text-sm">{healthMessage}</p>
929
- </div>
930
- ) : null}
931
- {healthDetailEntries.length > 0 ? (
932
- <div className="rounded-lg border p-3">
933
- <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
934
- {t('integrations.detail.health.details', 'Details')}
935
- </p>
936
- <dl className="mt-3 grid gap-3 sm:grid-cols-2">
937
- {healthDetailEntries.map(([key, value]) => (
938
- <div key={key}>
939
- <dt className="text-xs font-medium text-muted-foreground">{key}</dt>
940
- <dd className="mt-1 text-sm">{formatHealthValue(value)}</dd>
941
- </div>
942
- ))}
943
- </dl>
1047
+ {healthMessage || healthDetailEntries.length > 0 ? (
1048
+ <div className={`grid gap-3 ${healthMessage && healthDetailEntries.length > 0 ? 'xl:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)]' : ''}`}>
1049
+ {healthMessage ? (
1050
+ <div className="rounded-lg border px-4 py-3">
1051
+ <p className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
1052
+ {t('integrations.detail.health.lastResult', 'Last result')}
1053
+ </p>
1054
+ <p className="mt-1.5 text-sm">{healthMessage}</p>
1055
+ </div>
1056
+ ) : null}
1057
+ {healthDetailEntries.length > 0 ? (
1058
+ <div className="rounded-lg border px-4 py-3">
1059
+ <p className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
1060
+ {t('integrations.detail.health.details', 'Details')}
1061
+ </p>
1062
+ <dl className="mt-2 grid gap-x-6 gap-y-2 sm:grid-cols-2">
1063
+ {healthDetailEntries.map(([key, value]) => (
1064
+ <div key={key}>
1065
+ <dt className="text-xs font-medium text-muted-foreground">{formatLogDetailLabel(key)}</dt>
1066
+ <dd className="mt-0.5 text-sm">{formatHealthValue(value)}</dd>
1067
+ </div>
1068
+ ))}
1069
+ </dl>
1070
+ </div>
1071
+ ) : null}
944
1072
  </div>
945
1073
  ) : null}
946
- <p className="text-xs text-muted-foreground">
947
- {resolvedState.lastHealthCheckedAt
948
- ? t('integrations.detail.health.lastChecked', { date: new Date(resolvedState.lastHealthCheckedAt).toLocaleString() })
949
- : t('integrations.detail.health.neverChecked')
950
- }
951
- </p>
952
1074
  </CardContent>
953
1075
  </Card>
954
1076
  </TabsContent>
@@ -3,6 +3,14 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
4
4
  import { IntegrationState } from '../data/entities'
5
5
 
6
+ export type ResolvedIntegrationState = {
7
+ isEnabled: boolean
8
+ apiVersion: string | null
9
+ reauthRequired: boolean
10
+ lastHealthStatus: string | null
11
+ lastHealthCheckedAt: Date | null
12
+ }
13
+
6
14
  export function createIntegrationStateService(em: EntityManager) {
7
15
  return {
8
16
  async get(integrationId: string, scope: IntegrationScope): Promise<IntegrationState | null> {
@@ -20,6 +28,22 @@ export function createIntegrationStateService(em: EntityManager) {
20
28
  )
21
29
  },
22
30
 
31
+ async resolveState(integrationId: string, scope: IntegrationScope): Promise<ResolvedIntegrationState> {
32
+ const state = await this.get(integrationId, scope)
33
+ return {
34
+ isEnabled: state?.isEnabled ?? false,
35
+ apiVersion: state?.apiVersion ?? null,
36
+ reauthRequired: state?.reauthRequired ?? false,
37
+ lastHealthStatus: state?.lastHealthStatus ?? null,
38
+ lastHealthCheckedAt: state?.lastHealthCheckedAt ?? null,
39
+ }
40
+ },
41
+
42
+ async isEnabled(integrationId: string, scope: IntegrationScope): Promise<boolean> {
43
+ const state = await this.resolveState(integrationId, scope)
44
+ return state.isEnabled
45
+ },
46
+
23
47
  async upsert(
24
48
  integrationId: string,
25
49
  input: Partial<Pick<IntegrationState, 'isEnabled' | 'apiVersion' | 'reauthRequired' | 'lastHealthStatus' | 'lastHealthCheckedAt'>>,
@@ -38,7 +62,7 @@ export function createIntegrationStateService(em: EntityManager) {
38
62
 
39
63
  const created = em.create(IntegrationState, {
40
64
  integrationId,
41
- isEnabled: input.isEnabled ?? true,
65
+ isEnabled: input.isEnabled ?? false,
42
66
  apiVersion: input.apiVersion,
43
67
  reauthRequired: input.reauthRequired ?? false,
44
68
  lastHealthStatus: input.lastHealthStatus,
@@ -65,25 +65,8 @@ export async function GET(req: Request, { params }: { params: { id: string } })
65
65
  return Response.json({ error: 'Access denied' }, { status: 403 })
66
66
  }
67
67
 
68
- if (!skipMarkRead && recipient && recipient.status === 'unread') {
69
- const commandBus = ctx.container.resolve('commandBus') as CommandBus
70
- await commandBus.execute('messages.recipients.mark_read', {
71
- input: {
72
- messageId: params.id,
73
- tenantId: scope.tenantId,
74
- organizationId: scope.organizationId,
75
- userId: scope.userId,
76
- },
77
- ctx: {
78
- container: ctx.container,
79
- auth: ctx.auth ?? null,
80
- organizationScope: null,
81
- selectedOrganizationId: scope.organizationId,
82
- organizationIds: scope.organizationId ? [scope.organizationId] : null,
83
- request: req,
84
- },
85
- })
86
- }
68
+ const autoMarkRead = !skipMarkRead && recipient?.status === 'unread'
69
+ const readAt = autoMarkRead ? new Date() : recipient?.readAt ?? null
87
70
 
88
71
  const objects = await em.find(MessageObject, { messageId: params.id })
89
72
  const objectPreviews = await Promise.all(
@@ -162,6 +145,26 @@ export async function GET(req: Request, { params }: { params: { id: string } })
162
145
  const messageType = getMessageTypeOrDefault(message.type)
163
146
  const resolvedActionData = buildResolvedMessageActions(message, objects)
164
147
 
148
+ if (autoMarkRead) {
149
+ const commandBus = ctx.container.resolve('commandBus') as CommandBus
150
+ await commandBus.execute('messages.recipients.mark_read', {
151
+ input: {
152
+ messageId: params.id,
153
+ tenantId: scope.tenantId,
154
+ organizationId: scope.organizationId,
155
+ userId: scope.userId,
156
+ },
157
+ ctx: {
158
+ container: ctx.container,
159
+ auth: ctx.auth ?? null,
160
+ organizationScope: null,
161
+ selectedOrganizationId: scope.organizationId,
162
+ organizationIds: scope.organizationId ? [scope.organizationId] : null,
163
+ request: req,
164
+ },
165
+ })
166
+ }
167
+
165
168
  return Response.json({
166
169
  id: message.id,
167
170
  type: message.type,
@@ -201,8 +204,8 @@ export async function GET(req: Request, { params }: { params: { id: string } })
201
204
  recipients: allRecipients.map((item) => ({
202
205
  userId: item.recipientUserId,
203
206
  type: item.recipientType,
204
- status: item.status,
205
- readAt: item.readAt,
207
+ status: autoMarkRead && item.recipientUserId === scope.userId ? 'read' : item.status,
208
+ readAt: autoMarkRead && item.recipientUserId === scope.userId ? readAt : item.readAt,
206
209
  })),
207
210
  objects: objects.map((item, index) => ({
208
211
  id: item.id,
@@ -231,7 +234,7 @@ export async function GET(req: Request, { params }: { params: { id: string } })
231
234
  sentAt: threadMessage.sentAt,
232
235
  }
233
236
  }),
234
- isRead: recipient ? recipient.status !== 'unread' : true,
237
+ isRead: recipient ? (autoMarkRead || recipient.status !== 'unread') : true,
235
238
  })
236
239
  }
237
240
 
@@ -69,9 +69,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
69
69
  )
70
70
  : []
71
71
 
72
- let transaction = null as GatewayTransaction | null
73
- let matchedScope = null as { organizationId: string; tenantId: string } | null
74
- let event = null as Awaited<ReturnType<typeof registration.handler>> | null
72
+ let transaction: GatewayTransaction | null = null
73
+ let matchedScope: { organizationId: string; tenantId: string } | null = null
74
+ let event: Awaited<ReturnType<typeof registration.handler>> | null = null
75
75
  let lastVerificationError: unknown = null
76
76
 
77
77
  for (const candidate of candidates) {
@@ -4,7 +4,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
4
  import type { ProgressJob } from '../../data/entities'
5
5
 
6
6
  const routeMetadata = {
7
- GET: { requireAuth: true, requireFeatures: ['progress.view'] },
7
+ GET: { requireAuth: true },
8
8
  }
9
9
 
10
10
  export const metadata = routeMetadata
@@ -20,6 +20,8 @@ export async function GET(req: Request) {
20
20
 
21
21
  const ctx = { tenantId: auth.tenantId, organizationId: auth.orgId }
22
22
 
23
+ await progressService.markStaleJobsFailed(auth.tenantId)
24
+
23
25
  const [jobs, recentlyCompleted] = await Promise.all([
24
26
  progressService.getActiveJobs(ctx),
25
27
  progressService.getRecentlyCompletedJobs(ctx),
@@ -43,6 +45,7 @@ function formatJob(job: ProgressJob) {
43
45
  totalCount: job.totalCount,
44
46
  etaSeconds: job.etaSeconds,
45
47
  cancellable: job.cancellable,
48
+ meta: job.meta ?? null,
46
49
  startedAt: job.startedAt?.toISOString() ?? null,
47
50
  finishedAt: job.finishedAt?.toISOString() ?? null,
48
51
  errorMessage: job.errorMessage,
@@ -7,7 +7,7 @@ import { updateProgressSchema } from '../../../data/validators'
7
7
  import type { ProgressService } from '../../../lib/progressService'
8
8
 
9
9
  const routeMetadata = {
10
- GET: { requireAuth: true, requireFeatures: ['progress.view'] },
10
+ GET: { requireAuth: true },
11
11
  PUT: { requireAuth: true, requireFeatures: ['progress.update'] },
12
12
  DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },
13
13
  }
@@ -13,7 +13,7 @@ import {
13
13
  } from '../openapi'
14
14
 
15
15
  const routeMetadata = {
16
- GET: { requireAuth: true, requireFeatures: ['progress.view'] },
16
+ GET: { requireAuth: true },
17
17
  POST: { requireAuth: true, requireFeatures: ['progress.create'] },
18
18
  }
19
19
 
@@ -27,6 +27,7 @@ export type ProgressJobCreatedPayload = {
27
27
  cancellable?: boolean
28
28
  startedAt?: string | null
29
29
  finishedAt?: string | null
30
+ meta?: Record<string, unknown> | null
30
31
  tenantId: string
31
32
  organizationId?: string | null
32
33
  }
@@ -44,6 +45,7 @@ export type ProgressJobStartedPayload = {
44
45
  cancellable?: boolean
45
46
  startedAt?: string | null
46
47
  finishedAt?: string | null
48
+ meta?: Record<string, unknown> | null
47
49
  tenantId: string
48
50
  organizationId?: string | null
49
51
  }
@@ -63,6 +65,7 @@ export type ProgressJobUpdatedPayload = {
63
65
  cancellable?: boolean
64
66
  startedAt?: string | null
65
67
  finishedAt?: string | null
68
+ meta?: Record<string, unknown> | null
66
69
  }
67
70
 
68
71
  export type ProgressJobCompletedPayload = {
@@ -78,6 +81,7 @@ export type ProgressJobCompletedPayload = {
78
81
  cancellable?: boolean
79
82
  startedAt?: string | null
80
83
  finishedAt?: string | null
84
+ meta?: Record<string, unknown> | null
81
85
  resultSummary?: Record<string, unknown> | null
82
86
  tenantId: string
83
87
  organizationId?: string | null
@@ -96,6 +100,7 @@ export type ProgressJobFailedPayload = {
96
100
  cancellable?: boolean
97
101
  startedAt?: string | null
98
102
  finishedAt?: string | null
103
+ meta?: Record<string, unknown> | null
99
104
  errorMessage: string
100
105
  tenantId: string
101
106
  organizationId?: string | null
@@ -115,6 +120,7 @@ export type ProgressJobCancelledPayload = {
115
120
  cancellable?: boolean
116
121
  startedAt?: string | null
117
122
  finishedAt?: string | null
123
+ meta?: Record<string, unknown> | null
118
124
  tenantId: string
119
125
  organizationId?: string | null
120
126
  }
@@ -15,6 +15,7 @@ export interface ProgressService {
15
15
  completeJob(jobId: string, input: CompleteJobInput | undefined, ctx: ProgressServiceContext): Promise<ProgressJob>
16
16
  failJob(jobId: string, input: FailJobInput, ctx: ProgressServiceContext): Promise<ProgressJob>
17
17
  cancelJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>
18
+ markCancelled(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob>
18
19
  isCancellationRequested(jobId: string): Promise<boolean>
19
20
  getActiveJobs(ctx: ProgressServiceContext): Promise<ProgressJob[]>
20
21
  getRecentlyCompletedJobs(ctx: ProgressServiceContext, sinceSeconds?: number): Promise<ProgressJob[]>