@open-mercato/core 0.4.6-develop-6d72ec5960 → 0.4.6-develop-cd1e2a9a0e

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 (226) hide show
  1. package/AGENTS.md +10 -0
  2. package/dist/generated/entities/integration_credentials/index.js +19 -0
  3. package/dist/generated/entities/integration_credentials/index.js.map +7 -0
  4. package/dist/generated/entities/integration_log/index.js +27 -0
  5. package/dist/generated/entities/integration_log/index.js.map +7 -0
  6. package/dist/generated/entities/integration_state/index.js +27 -0
  7. package/dist/generated/entities/integration_state/index.js.map +7 -0
  8. package/dist/generated/entities/sync_cursor/index.js +19 -0
  9. package/dist/generated/entities/sync_cursor/index.js.map +7 -0
  10. package/dist/generated/entities/sync_external_id_mapping/index.js +27 -0
  11. package/dist/generated/entities/sync_external_id_mapping/index.js.map +7 -0
  12. package/dist/generated/entities/sync_mapping/index.js +19 -0
  13. package/dist/generated/entities/sync_mapping/index.js.map +7 -0
  14. package/dist/generated/entities/sync_run/index.js +45 -0
  15. package/dist/generated/entities/sync_run/index.js.map +7 -0
  16. package/dist/generated/entities/sync_schedule/index.js +35 -0
  17. package/dist/generated/entities/sync_schedule/index.js.map +7 -0
  18. package/dist/generated/entities.ids.generated.js +14 -0
  19. package/dist/generated/entities.ids.generated.js.map +2 -2
  20. package/dist/generated/entity-fields-registry.js +16 -0
  21. package/dist/generated/entity-fields-registry.js.map +2 -2
  22. package/dist/modules/data_sync/acl.js +11 -0
  23. package/dist/modules/data_sync/acl.js.map +7 -0
  24. package/dist/modules/data_sync/api/mappings/[id]/route.js +137 -0
  25. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +7 -0
  26. package/dist/modules/data_sync/api/mappings/route.js +132 -0
  27. package/dist/modules/data_sync/api/mappings/route.js.map +7 -0
  28. package/dist/modules/data_sync/api/run.js +87 -0
  29. package/dist/modules/data_sync/api/run.js.map +7 -0
  30. package/dist/modules/data_sync/api/runs/[id]/cancel.js +49 -0
  31. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +7 -0
  32. package/dist/modules/data_sync/api/runs/[id]/retry.js +93 -0
  33. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +7 -0
  34. package/dist/modules/data_sync/api/runs/[id]/route.js +69 -0
  35. package/dist/modules/data_sync/api/runs/[id]/route.js.map +7 -0
  36. package/dist/modules/data_sync/api/runs.js +66 -0
  37. package/dist/modules/data_sync/api/runs.js.map +7 -0
  38. package/dist/modules/data_sync/api/validate.js +66 -0
  39. package/dist/modules/data_sync/api/validate.js.map +7 -0
  40. package/dist/modules/data_sync/backend/data-sync/page.js +216 -0
  41. package/dist/modules/data_sync/backend/data-sync/page.js.map +7 -0
  42. package/dist/modules/data_sync/backend/data-sync/page.meta.js +25 -0
  43. package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +7 -0
  44. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +178 -0
  45. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +7 -0
  46. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js +14 -0
  47. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js.map +7 -0
  48. package/dist/modules/data_sync/data/entities.js +228 -0
  49. package/dist/modules/data_sync/data/entities.js.map +7 -0
  50. package/dist/modules/data_sync/data/validators.js +32 -0
  51. package/dist/modules/data_sync/data/validators.js.map +7 -0
  52. package/dist/modules/data_sync/di.js +26 -0
  53. package/dist/modules/data_sync/di.js.map +7 -0
  54. package/dist/modules/data_sync/events.js +16 -0
  55. package/dist/modules/data_sync/events.js.map +7 -0
  56. package/dist/modules/data_sync/index.js +9 -0
  57. package/dist/modules/data_sync/index.js.map +7 -0
  58. package/dist/modules/data_sync/lib/adapter-registry.js +16 -0
  59. package/dist/modules/data_sync/lib/adapter-registry.js.map +7 -0
  60. package/dist/modules/data_sync/lib/adapter.js +1 -0
  61. package/dist/modules/data_sync/lib/adapter.js.map +7 -0
  62. package/dist/modules/data_sync/lib/id-mapping.js +79 -0
  63. package/dist/modules/data_sync/lib/id-mapping.js.map +7 -0
  64. package/dist/modules/data_sync/lib/queue.js +17 -0
  65. package/dist/modules/data_sync/lib/queue.js.map +7 -0
  66. package/dist/modules/data_sync/lib/sync-engine.js +309 -0
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +7 -0
  68. package/dist/modules/data_sync/lib/sync-run-service.js +148 -0
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +7 -0
  70. package/dist/modules/data_sync/migrations/Migration20260304113737.js +17 -0
  71. package/dist/modules/data_sync/migrations/Migration20260304113737.js.map +7 -0
  72. package/dist/modules/data_sync/setup.js +13 -0
  73. package/dist/modules/data_sync/setup.js.map +7 -0
  74. package/dist/modules/data_sync/workers/sync-export.js +14 -0
  75. package/dist/modules/data_sync/workers/sync-export.js.map +7 -0
  76. package/dist/modules/data_sync/workers/sync-import.js +14 -0
  77. package/dist/modules/data_sync/workers/sync-import.js.map +7 -0
  78. package/dist/modules/data_sync/workers/sync-scheduled.js +63 -0
  79. package/dist/modules/data_sync/workers/sync-scheduled.js.map +7 -0
  80. package/dist/modules/entities/lib/encryptionDefaults.js +4 -0
  81. package/dist/modules/entities/lib/encryptionDefaults.js.map +2 -2
  82. package/dist/modules/integrations/acl.js +4 -1
  83. package/dist/modules/integrations/acl.js.map +2 -2
  84. package/dist/modules/integrations/api/[id]/credentials/route.js +127 -0
  85. package/dist/modules/integrations/api/[id]/credentials/route.js.map +7 -0
  86. package/dist/modules/integrations/api/[id]/health/route.js +46 -0
  87. package/dist/modules/integrations/api/[id]/health/route.js.map +7 -0
  88. package/dist/modules/integrations/api/[id]/route.js +65 -0
  89. package/dist/modules/integrations/api/[id]/route.js.map +7 -0
  90. package/dist/modules/integrations/api/[id]/state/route.js +109 -0
  91. package/dist/modules/integrations/api/[id]/state/route.js.map +7 -0
  92. package/dist/modules/integrations/api/[id]/version/route.js +117 -0
  93. package/dist/modules/integrations/api/[id]/version/route.js.map +7 -0
  94. package/dist/modules/integrations/api/guards.js +31 -0
  95. package/dist/modules/integrations/api/guards.js.map +7 -0
  96. package/dist/modules/integrations/api/logs/route.js +60 -0
  97. package/dist/modules/integrations/api/logs/route.js.map +7 -0
  98. package/dist/modules/integrations/api/openapi.js +25 -0
  99. package/dist/modules/integrations/api/openapi.js.map +7 -0
  100. package/dist/modules/integrations/api/route.js +68 -0
  101. package/dist/modules/integrations/api/route.js.map +7 -0
  102. package/dist/modules/integrations/backend/integrations/[id]/page.js +313 -0
  103. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +7 -0
  104. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js +15 -0
  105. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js.map +7 -0
  106. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +189 -0
  107. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +7 -0
  108. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js +15 -0
  109. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js.map +7 -0
  110. package/dist/modules/integrations/backend/integrations/page.js +212 -0
  111. package/dist/modules/integrations/backend/integrations/page.js.map +7 -0
  112. package/dist/modules/integrations/backend/integrations/page.meta.js +22 -0
  113. package/dist/modules/integrations/backend/integrations/page.meta.js.map +7 -0
  114. package/dist/modules/integrations/data/enrichers.js +27 -12
  115. package/dist/modules/integrations/data/enrichers.js.map +2 -2
  116. package/dist/modules/integrations/data/entities.js +136 -1
  117. package/dist/modules/integrations/data/entities.js.map +2 -2
  118. package/dist/modules/integrations/data/validators.js +36 -0
  119. package/dist/modules/integrations/data/validators.js.map +7 -0
  120. package/dist/modules/integrations/di.js +24 -0
  121. package/dist/modules/integrations/di.js.map +7 -0
  122. package/dist/modules/integrations/events.js +19 -0
  123. package/dist/modules/integrations/events.js.map +7 -0
  124. package/dist/modules/integrations/lib/credentials-service.js +159 -0
  125. package/dist/modules/integrations/lib/credentials-service.js.map +7 -0
  126. package/dist/modules/integrations/lib/health-service.js +37 -0
  127. package/dist/modules/integrations/lib/health-service.js.map +7 -0
  128. package/dist/modules/integrations/lib/log-service.js +66 -0
  129. package/dist/modules/integrations/lib/log-service.js.map +7 -0
  130. package/dist/modules/integrations/lib/registry-service.js +33 -0
  131. package/dist/modules/integrations/lib/registry-service.js.map +7 -0
  132. package/dist/modules/integrations/lib/state-service.js +55 -0
  133. package/dist/modules/integrations/lib/state-service.js.map +7 -0
  134. package/dist/modules/integrations/lib/types.js +1 -0
  135. package/dist/modules/integrations/lib/types.js.map +7 -0
  136. package/dist/modules/integrations/migrations/Migration20260304113737.js +19 -0
  137. package/dist/modules/integrations/migrations/Migration20260304113737.js.map +7 -0
  138. package/dist/modules/integrations/setup.js +2 -2
  139. package/dist/modules/integrations/setup.js.map +2 -2
  140. package/dist/modules/integrations/widgets/injection-table.js.map +1 -1
  141. package/dist/modules/integrations/workers/log-pruner.js +18 -0
  142. package/dist/modules/integrations/workers/log-pruner.js.map +7 -0
  143. package/generated/entities/integration_credentials/index.ts +8 -0
  144. package/generated/entities/integration_log/index.ts +12 -0
  145. package/generated/entities/integration_state/index.ts +12 -0
  146. package/generated/entities/sync_cursor/index.ts +8 -0
  147. package/generated/entities/sync_external_id_mapping/index.ts +12 -0
  148. package/generated/entities/sync_mapping/index.ts +8 -0
  149. package/generated/entities/sync_run/index.ts +21 -0
  150. package/generated/entities/sync_schedule/index.ts +16 -0
  151. package/generated/entities.ids.generated.ts +14 -0
  152. package/generated/entity-fields-registry.ts +16 -0
  153. package/package.json +2 -2
  154. package/src/modules/data_sync/AGENTS.md +157 -0
  155. package/src/modules/data_sync/acl.ts +7 -0
  156. package/src/modules/data_sync/api/mappings/[id]/route.ts +158 -0
  157. package/src/modules/data_sync/api/mappings/route.ts +144 -0
  158. package/src/modules/data_sync/api/run.ts +97 -0
  159. package/src/modules/data_sync/api/runs/[id]/cancel.ts +57 -0
  160. package/src/modules/data_sync/api/runs/[id]/retry.ts +108 -0
  161. package/src/modules/data_sync/api/runs/[id]/route.ts +81 -0
  162. package/src/modules/data_sync/api/runs.ts +69 -0
  163. package/src/modules/data_sync/api/validate.ts +73 -0
  164. package/src/modules/data_sync/backend/data-sync/page.meta.ts +21 -0
  165. package/src/modules/data_sync/backend/data-sync/page.tsx +244 -0
  166. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.meta.ts +10 -0
  167. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +278 -0
  168. package/src/modules/data_sync/data/entities.ts +180 -0
  169. package/src/modules/data_sync/data/validators.ts +35 -0
  170. package/src/modules/data_sync/di.ts +38 -0
  171. package/src/modules/data_sync/events.ts +12 -0
  172. package/src/modules/data_sync/i18n/de.json +48 -0
  173. package/src/modules/data_sync/i18n/en.json +48 -0
  174. package/src/modules/data_sync/i18n/es.json +48 -0
  175. package/src/modules/data_sync/i18n/pl.json +48 -0
  176. package/src/modules/data_sync/index.ts +5 -0
  177. package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
  178. package/src/modules/data_sync/lib/adapter.ts +90 -0
  179. package/src/modules/data_sync/lib/id-mapping.ts +95 -0
  180. package/src/modules/data_sync/lib/queue.ts +19 -0
  181. package/src/modules/data_sync/lib/sync-engine.ts +375 -0
  182. package/src/modules/data_sync/lib/sync-run-service.ts +187 -0
  183. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +653 -0
  184. package/src/modules/data_sync/migrations/Migration20260304113737.ts +19 -0
  185. package/src/modules/data_sync/setup.ts +11 -0
  186. package/src/modules/data_sync/workers/sync-export.ts +27 -0
  187. package/src/modules/data_sync/workers/sync-import.ts +27 -0
  188. package/src/modules/data_sync/workers/sync-scheduled.ts +84 -0
  189. package/src/modules/entities/lib/encryptionDefaults.ts +4 -0
  190. package/src/modules/integrations/AGENTS.md +160 -0
  191. package/src/modules/integrations/acl.ts +3 -0
  192. package/src/modules/integrations/api/[id]/credentials/route.ts +142 -0
  193. package/src/modules/integrations/api/[id]/health/route.ts +53 -0
  194. package/src/modules/integrations/api/[id]/route.ts +76 -0
  195. package/src/modules/integrations/api/[id]/state/route.ts +121 -0
  196. package/src/modules/integrations/api/[id]/version/route.ts +132 -0
  197. package/src/modules/integrations/api/guards.ts +59 -0
  198. package/src/modules/integrations/api/logs/route.ts +63 -0
  199. package/src/modules/integrations/api/openapi.ts +22 -0
  200. package/src/modules/integrations/api/route.ts +73 -0
  201. package/src/modules/integrations/backend/integrations/[id]/page.meta.ts +11 -0
  202. package/src/modules/integrations/backend/integrations/[id]/page.tsx +424 -0
  203. package/src/modules/integrations/backend/integrations/bundle/[id]/page.meta.ts +11 -0
  204. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +249 -0
  205. package/src/modules/integrations/backend/integrations/page.meta.ts +18 -0
  206. package/src/modules/integrations/backend/integrations/page.tsx +296 -0
  207. package/src/modules/integrations/data/enrichers.ts +35 -18
  208. package/src/modules/integrations/data/entities.ts +114 -5
  209. package/src/modules/integrations/data/validators.ts +41 -0
  210. package/src/modules/integrations/di.ts +31 -0
  211. package/src/modules/integrations/events.ts +17 -0
  212. package/src/modules/integrations/i18n/de.json +70 -0
  213. package/src/modules/integrations/i18n/en.json +70 -0
  214. package/src/modules/integrations/i18n/es.json +70 -0
  215. package/src/modules/integrations/i18n/pl.json +70 -0
  216. package/src/modules/integrations/lib/credentials-service.ts +204 -0
  217. package/src/modules/integrations/lib/health-service.ts +59 -0
  218. package/src/modules/integrations/lib/log-service.ts +84 -0
  219. package/src/modules/integrations/lib/registry-service.ts +42 -0
  220. package/src/modules/integrations/lib/state-service.ts +64 -0
  221. package/src/modules/integrations/lib/types.ts +4 -0
  222. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +582 -0
  223. package/src/modules/integrations/migrations/Migration20260304113737.ts +21 -0
  224. package/src/modules/integrations/setup.ts +2 -2
  225. package/src/modules/integrations/widgets/injection-table.ts +1 -1
  226. package/src/modules/integrations/workers/log-pruner.ts +30 -0
@@ -0,0 +1,244 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
6
+ import type { ColumnDef } from '@tanstack/react-table'
7
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
8
+ import { Badge } from '@open-mercato/ui/primitives/badge'
9
+ import { RowActions } from '@open-mercato/ui/backend/RowActions'
10
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
13
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+
15
+ type SyncRunRow = {
16
+ id: string
17
+ integrationId: string
18
+ entityType: string
19
+ direction: 'import' | 'export'
20
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
21
+ createdCount: number
22
+ updatedCount: number
23
+ failedCount: number
24
+ createdAt: string
25
+ }
26
+
27
+ type ResponsePayload = {
28
+ items: SyncRunRow[]
29
+ total: number
30
+ page: number
31
+ totalPages: number
32
+ }
33
+
34
+ const STATUS_STYLES: Record<string, string> = {
35
+ pending: 'bg-gray-100 text-gray-800',
36
+ running: 'bg-blue-100 text-blue-800',
37
+ completed: 'bg-green-100 text-green-800',
38
+ failed: 'bg-red-100 text-red-800',
39
+ cancelled: 'bg-yellow-100 text-yellow-800',
40
+ paused: 'bg-orange-100 text-orange-800',
41
+ }
42
+
43
+ export default function SyncRunsDashboardPage() {
44
+ const router = useRouter()
45
+ const [rows, setRows] = React.useState<SyncRunRow[]>([])
46
+ const [page, setPage] = React.useState(1)
47
+ const [total, setTotal] = React.useState(0)
48
+ const [totalPages, setTotalPages] = React.useState(1)
49
+ const [search, setSearch] = React.useState('')
50
+ const [filterValues, setFilterValues] = React.useState<FilterValues>({})
51
+ const [isLoading, setIsLoading] = React.useState(true)
52
+ const [reloadToken, setReloadToken] = React.useState(0)
53
+ const scopeVersion = useOrganizationScopeVersion()
54
+ const t = useT()
55
+
56
+ React.useEffect(() => {
57
+ let cancelled = false
58
+ async function load() {
59
+ setIsLoading(true)
60
+ const params = new URLSearchParams()
61
+ params.set('page', String(page))
62
+ params.set('pageSize', '20')
63
+ if (filterValues.status) params.set('status', filterValues.status as string)
64
+ if (filterValues.direction) params.set('direction', filterValues.direction as string)
65
+ const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }
66
+ const call = await apiCall<ResponsePayload>(
67
+ `/api/data_sync/runs?${params.toString()}`,
68
+ undefined,
69
+ { fallback },
70
+ )
71
+ if (!call.ok) {
72
+ flash(t('data_sync.dashboard.loadError'), 'error')
73
+ if (!cancelled) setIsLoading(false)
74
+ return
75
+ }
76
+ const payload = call.result ?? fallback
77
+ if (!cancelled) {
78
+ setRows(Array.isArray(payload.items) ? payload.items : [])
79
+ setTotal(payload.total || 0)
80
+ setTotalPages(payload.totalPages || 1)
81
+ setIsLoading(false)
82
+ }
83
+ }
84
+ load()
85
+ return () => { cancelled = true }
86
+ }, [page, filterValues, reloadToken, scopeVersion, t])
87
+
88
+ const handleCancel = React.useCallback(async (row: SyncRunRow) => {
89
+ const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {
90
+ method: 'POST',
91
+ }, { fallback: null })
92
+ if (call.ok) {
93
+ flash(t('data_sync.runs.detail.cancelSuccess'), 'success')
94
+ setReloadToken((token) => token + 1)
95
+ } else {
96
+ flash(t('data_sync.runs.detail.cancelError'), 'error')
97
+ }
98
+ }, [t])
99
+
100
+ const handleRetry = React.useCallback(async (row: SyncRunRow) => {
101
+ const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/retry`, {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ fromBeginning: false }),
105
+ }, { fallback: null })
106
+ if (call.ok) {
107
+ flash(t('data_sync.runs.detail.retrySuccess'), 'success')
108
+ setReloadToken((token) => token + 1)
109
+ } else {
110
+ flash(t('data_sync.runs.detail.retryError'), 'error')
111
+ }
112
+ }, [t])
113
+
114
+ const handleFiltersApply = React.useCallback((values: FilterValues) => {
115
+ const next: FilterValues = {}
116
+ Object.entries(values).forEach(([key, value]) => {
117
+ if (value !== undefined && value !== '') next[key] = value
118
+ })
119
+ setFilterValues(next)
120
+ setPage(1)
121
+ }, [])
122
+
123
+ const handleFiltersClear = React.useCallback(() => {
124
+ setFilterValues({})
125
+ setPage(1)
126
+ }, [])
127
+
128
+ const filters: FilterDef[] = [
129
+ {
130
+ id: 'status',
131
+ type: 'select',
132
+ label: t('data_sync.dashboard.filters.status'),
133
+ options: [
134
+ { label: t('data_sync.dashboard.filters.allStatuses'), value: '' },
135
+ { label: t('data_sync.dashboard.status.pending'), value: 'pending' },
136
+ { label: t('data_sync.dashboard.status.running'), value: 'running' },
137
+ { label: t('data_sync.dashboard.status.completed'), value: 'completed' },
138
+ { label: t('data_sync.dashboard.status.failed'), value: 'failed' },
139
+ { label: t('data_sync.dashboard.status.cancelled'), value: 'cancelled' },
140
+ ],
141
+ },
142
+ {
143
+ id: 'direction',
144
+ type: 'select',
145
+ label: t('data_sync.dashboard.columns.direction'),
146
+ options: [
147
+ { label: t('data_sync.dashboard.filters.allDirections'), value: '' },
148
+ { label: t('data_sync.dashboard.direction.import'), value: 'import' },
149
+ { label: t('data_sync.dashboard.direction.export'), value: 'export' },
150
+ ],
151
+ },
152
+ ]
153
+
154
+ const columns = React.useMemo<ColumnDef<SyncRunRow>[]>(() => [
155
+ {
156
+ accessorKey: 'integrationId',
157
+ header: t('data_sync.dashboard.columns.integration'),
158
+ cell: ({ row }) => <span className="font-medium text-sm">{row.original.integrationId}</span>,
159
+ },
160
+ {
161
+ accessorKey: 'entityType',
162
+ header: t('data_sync.dashboard.columns.entityType'),
163
+ },
164
+ {
165
+ accessorKey: 'direction',
166
+ header: t('data_sync.dashboard.columns.direction'),
167
+ cell: ({ row }) => (
168
+ <Badge variant="outline">
169
+ {t(`data_sync.dashboard.direction.${row.original.direction}`)}
170
+ </Badge>
171
+ ),
172
+ },
173
+ {
174
+ accessorKey: 'status',
175
+ header: t('data_sync.dashboard.columns.status'),
176
+ cell: ({ row }) => (
177
+ <Badge variant="secondary" className={STATUS_STYLES[row.original.status] ?? ''}>
178
+ {t(`data_sync.dashboard.status.${row.original.status}`)}
179
+ </Badge>
180
+ ),
181
+ },
182
+ {
183
+ accessorKey: 'createdCount',
184
+ header: t('data_sync.dashboard.columns.created'),
185
+ },
186
+ {
187
+ accessorKey: 'updatedCount',
188
+ header: t('data_sync.dashboard.columns.updated'),
189
+ },
190
+ {
191
+ accessorKey: 'failedCount',
192
+ header: t('data_sync.dashboard.columns.failed'),
193
+ },
194
+ {
195
+ accessorKey: 'createdAt',
196
+ header: t('data_sync.dashboard.columns.createdAt'),
197
+ cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),
198
+ },
199
+ ], [t])
200
+
201
+ return (
202
+ <Page>
203
+ <PageBody>
204
+ <DataTable
205
+ title={t('data_sync.dashboard.title')}
206
+ columns={columns}
207
+ data={rows}
208
+ filters={filters}
209
+ filterValues={filterValues}
210
+ onFiltersApply={handleFiltersApply}
211
+ onFiltersClear={handleFiltersClear}
212
+ searchValue={search}
213
+ onSearchChange={(value) => { setSearch(value); setPage(1) }}
214
+ perspective={{ tableId: 'data_sync.runs' }}
215
+ onRowClick={(row) => {
216
+ router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`)
217
+ }}
218
+ rowActions={(row) => (
219
+ <RowActions items={[
220
+ {
221
+ id: 'view',
222
+ label: t('data_sync.dashboard.actions.view'),
223
+ onSelect: () => { router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`) },
224
+ },
225
+ ...(row.status === 'running' ? [{
226
+ id: 'cancel',
227
+ label: t('data_sync.runs.detail.cancel'),
228
+ destructive: true,
229
+ onSelect: () => { void handleCancel(row) },
230
+ }] : []),
231
+ ...(row.status === 'failed' ? [{
232
+ id: 'retry',
233
+ label: t('data_sync.runs.detail.retry'),
234
+ onSelect: () => { void handleRetry(row) },
235
+ }] : []),
236
+ ]} />
237
+ )}
238
+ pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}
239
+ isLoading={isLoading}
240
+ />
241
+ </PageBody>
242
+ </Page>
243
+ )
244
+ }
@@ -0,0 +1,10 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['data_sync.view'],
4
+ pageTitle: 'Sync Run Detail',
5
+ pageTitleKey: 'data_sync.runs.detail.title',
6
+ navHidden: true,
7
+ breadcrumb: [
8
+ { label: 'Data Sync', labelKey: 'data_sync.nav.title', href: '/backend/data-sync' },
9
+ ],
10
+ }
@@ -0,0 +1,278 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { useParams, useRouter } from 'next/navigation'
5
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
+ import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
7
+ import { Badge } from '@open-mercato/ui/primitives/badge'
8
+ import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { Progress } from '@open-mercato/ui/primitives/progress'
10
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
12
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { LoadingMessage } from '@open-mercato/ui/backend/detail'
15
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
16
+
17
+ type SyncRunDetail = {
18
+ id: string
19
+ integrationId: string
20
+ entityType: string
21
+ direction: 'import' | 'export'
22
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
23
+ createdCount: number
24
+ updatedCount: number
25
+ skippedCount: number
26
+ failedCount: number
27
+ batchesCompleted: number
28
+ lastError: string | null
29
+ progressJobId: string | null
30
+ progressJob: {
31
+ id: string
32
+ status: string
33
+ progressPercent: number
34
+ processedCount: number
35
+ totalCount: number | null
36
+ etaSeconds: number | null
37
+ } | null
38
+ triggeredBy: string | null
39
+ createdAt: string
40
+ updatedAt: string
41
+ }
42
+
43
+ type LogEntry = {
44
+ id: string
45
+ level: 'info' | 'warn' | 'error'
46
+ message: string
47
+ createdAt: string
48
+ }
49
+
50
+ const STATUS_STYLES: Record<string, string> = {
51
+ pending: 'bg-gray-100 text-gray-800',
52
+ running: 'bg-blue-100 text-blue-800',
53
+ completed: 'bg-green-100 text-green-800',
54
+ failed: 'bg-red-100 text-red-800',
55
+ cancelled: 'bg-yellow-100 text-yellow-800',
56
+ paused: 'bg-orange-100 text-orange-800',
57
+ }
58
+
59
+ const LOG_LEVEL_STYLES: Record<string, string> = {
60
+ info: 'bg-blue-100 text-blue-800',
61
+ warn: 'bg-yellow-100 text-yellow-800',
62
+ error: 'bg-red-100 text-red-800',
63
+ }
64
+
65
+ export default function SyncRunDetailPage() {
66
+ const params = useParams<{ id: string }>()
67
+ const router = useRouter()
68
+ const runId = params.id
69
+ const t = useT()
70
+
71
+ const [run, setRun] = React.useState<SyncRunDetail | null>(null)
72
+ const [isLoading, setIsLoading] = React.useState(true)
73
+ const [error, setError] = React.useState<string | null>(null)
74
+ const [logs, setLogs] = React.useState<LogEntry[]>([])
75
+ const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)
76
+
77
+ const loadRun = React.useCallback(async () => {
78
+ const call = await apiCall<SyncRunDetail>(
79
+ `/api/data_sync/runs/${encodeURIComponent(runId)}`,
80
+ undefined,
81
+ { fallback: null },
82
+ )
83
+ if (!call.ok || !call.result) {
84
+ setError(t('data_sync.runs.detail.loadError'))
85
+ setIsLoading(false)
86
+ return
87
+ }
88
+ setRun(call.result)
89
+ setIsLoading(false)
90
+ }, [runId, t])
91
+
92
+ const loadLogs = React.useCallback(async () => {
93
+ setIsLoadingLogs(true)
94
+ const params = new URLSearchParams({ runId, pageSize: '50' })
95
+ const call = await apiCall<{ items: LogEntry[] }>(
96
+ `/api/integrations/logs?${params.toString()}`,
97
+ undefined,
98
+ { fallback: { items: [] } },
99
+ )
100
+ if (call.ok && call.result) {
101
+ setLogs(call.result.items)
102
+ }
103
+ setIsLoadingLogs(false)
104
+ }, [runId])
105
+
106
+ React.useEffect(() => {
107
+ void loadRun()
108
+ void loadLogs()
109
+ }, [loadRun, loadLogs])
110
+
111
+ React.useEffect(() => {
112
+ if (!run || (run.status !== 'running' && run.status !== 'pending')) return
113
+ const interval = setInterval(() => { void loadRun() }, 4000)
114
+ return () => clearInterval(interval)
115
+ }, [run?.status, loadRun])
116
+
117
+ const handleCancel = React.useCallback(async () => {
118
+ const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(runId)}/cancel`, {
119
+ method: 'POST',
120
+ }, { fallback: null })
121
+ if (call.ok) {
122
+ flash(t('data_sync.runs.detail.cancelSuccess'), 'success')
123
+ void loadRun()
124
+ } else {
125
+ flash(t('data_sync.runs.detail.cancelError'), 'error')
126
+ }
127
+ }, [runId, t, loadRun])
128
+
129
+ const handleRetry = React.useCallback(async () => {
130
+ const call = await apiCall<{ id: string }>(`/api/data_sync/runs/${encodeURIComponent(runId)}/retry`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({ fromBeginning: false }),
134
+ }, { fallback: null })
135
+ if (call.ok && call.result) {
136
+ flash(t('data_sync.runs.detail.retrySuccess'), 'success')
137
+ router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)
138
+ } else {
139
+ flash(t('data_sync.runs.detail.retryError'), 'error')
140
+ }
141
+ }, [router, runId, t])
142
+
143
+ if (isLoading) return <Page><PageBody><LoadingMessage label={t('data_sync.runs.detail.title')} /></PageBody></Page>
144
+ if (error || !run) return <Page><PageBody><ErrorMessage label={error ?? t('data_sync.runs.detail.loadError')} /></PageBody></Page>
145
+
146
+ const totalProcessed = run.createdCount + run.updatedCount + run.skippedCount + run.failedCount
147
+ const progressPercent = run.progressJob?.progressPercent ?? (run.status === 'completed' ? 100 : 0)
148
+
149
+ return (
150
+ <Page>
151
+ <PageBody className="space-y-6">
152
+ <div>
153
+ <Link href="/backend/data-sync" className="text-sm text-muted-foreground hover:underline">
154
+ {t('data_sync.runs.detail.back')}
155
+ </Link>
156
+ </div>
157
+
158
+ <div className="flex items-center justify-between">
159
+ <div>
160
+ <h1 className="text-2xl font-semibold">{run.integrationId} — {run.entityType}</h1>
161
+ <div className="flex gap-2 mt-2">
162
+ <Badge variant="outline">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>
163
+ <Badge variant="secondary" className={STATUS_STYLES[run.status] ?? ''}>
164
+ {t(`data_sync.dashboard.status.${run.status}`)}
165
+ </Badge>
166
+ {run.triggeredBy && <Badge variant="outline">{run.triggeredBy}</Badge>}
167
+ </div>
168
+ </div>
169
+ <div className="flex gap-2">
170
+ {(run.status === 'running' || run.status === 'pending') && (
171
+ <Button type="button" variant="destructive" size="sm" onClick={() => void handleCancel()}>
172
+ {t('data_sync.runs.detail.cancel')}
173
+ </Button>
174
+ )}
175
+ {run.status === 'failed' && (
176
+ <Button type="button" variant="outline" size="sm" onClick={() => void handleRetry()}>
177
+ {t('data_sync.runs.detail.retry')}
178
+ </Button>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ {(run.status === 'running' || run.status === 'pending') && (
184
+ <Card>
185
+ <CardHeader>
186
+ <CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>
187
+ </CardHeader>
188
+ <CardContent className="space-y-3">
189
+ <Progress value={progressPercent} className="h-3" />
190
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
191
+ <span>{t('data_sync.runs.detail.progress.itemsProcessed', { count: totalProcessed })}</span>
192
+ <span>{t('data_sync.runs.detail.progress.batches', { count: run.batchesCompleted })}</span>
193
+ </div>
194
+ </CardContent>
195
+ </Card>
196
+ )}
197
+
198
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
199
+ <Card>
200
+ <CardContent className="pt-6 text-center">
201
+ <div className="text-2xl font-bold text-green-600">{run.createdCount}</div>
202
+ <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.created')}</p>
203
+ </CardContent>
204
+ </Card>
205
+ <Card>
206
+ <CardContent className="pt-6 text-center">
207
+ <div className="text-2xl font-bold text-blue-600">{run.updatedCount}</div>
208
+ <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.updated')}</p>
209
+ </CardContent>
210
+ </Card>
211
+ <Card>
212
+ <CardContent className="pt-6 text-center">
213
+ <div className="text-2xl font-bold text-gray-600">{run.skippedCount}</div>
214
+ <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.skipped')}</p>
215
+ </CardContent>
216
+ </Card>
217
+ <Card>
218
+ <CardContent className="pt-6 text-center">
219
+ <div className="text-2xl font-bold text-red-600">{run.failedCount}</div>
220
+ <p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.failed')}</p>
221
+ </CardContent>
222
+ </Card>
223
+ </div>
224
+
225
+ {run.lastError && (
226
+ <Card className="border-red-200 bg-red-50">
227
+ <CardHeader>
228
+ <CardTitle className="text-red-800">{t('data_sync.runs.detail.error')}</CardTitle>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <pre className="text-sm text-red-700 whitespace-pre-wrap">{run.lastError}</pre>
232
+ </CardContent>
233
+ </Card>
234
+ )}
235
+
236
+ <Card>
237
+ <CardHeader>
238
+ <CardTitle>{t('data_sync.runs.detail.logs')}</CardTitle>
239
+ </CardHeader>
240
+ <CardContent>
241
+ {isLoadingLogs ? (
242
+ <div className="flex justify-center py-4"><Spinner /></div>
243
+ ) : logs.length === 0 ? (
244
+ <p className="text-muted-foreground text-sm">{t('data_sync.runs.detail.noLogs')}</p>
245
+ ) : (
246
+ <div className="rounded-lg border">
247
+ <table className="w-full text-sm">
248
+ <thead>
249
+ <tr className="border-b bg-muted/50">
250
+ <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.time')}</th>
251
+ <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.level')}</th>
252
+ <th className="px-4 py-2 text-left font-medium">{t('data_sync.runs.detail.logs.message')}</th>
253
+ </tr>
254
+ </thead>
255
+ <tbody>
256
+ {logs.map((log) => (
257
+ <tr key={log.id} className="border-b last:border-0">
258
+ <td className="px-4 py-2 text-muted-foreground whitespace-nowrap">
259
+ {new Date(log.createdAt).toLocaleString()}
260
+ </td>
261
+ <td className="px-4 py-2">
262
+ <Badge variant="secondary" className={LOG_LEVEL_STYLES[log.level] ?? ''}>
263
+ {log.level}
264
+ </Badge>
265
+ </td>
266
+ <td className="px-4 py-2">{log.message}</td>
267
+ </tr>
268
+ ))}
269
+ </tbody>
270
+ </table>
271
+ </div>
272
+ )}
273
+ </CardContent>
274
+ </Card>
275
+ </PageBody>
276
+ </Page>
277
+ )
278
+ }