@open-mercato/ui 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6472.1.1673e7e66b

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 (81) hide show
  1. package/dist/ai/AiChat.js +3 -3
  2. package/dist/ai/AiChat.js.map +2 -2
  3. package/dist/backend/CrudForm.js +19 -17
  4. package/dist/backend/CrudForm.js.map +2 -2
  5. package/dist/backend/DataTable.js +5 -5
  6. package/dist/backend/DataTable.js.map +2 -2
  7. package/dist/backend/JsonDisplay.js +3 -1
  8. package/dist/backend/JsonDisplay.js.map +2 -2
  9. package/dist/backend/confirm-dialog/useConfirmDialog.js +3 -3
  10. package/dist/backend/confirm-dialog/useConfirmDialog.js.map +2 -2
  11. package/dist/backend/dashboard/DashboardScreen.js +8 -6
  12. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  13. package/dist/backend/dashboard/widgetRegistry.js +3 -1
  14. package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
  15. package/dist/backend/detail/CustomDataSection.js +4 -2
  16. package/dist/backend/detail/CustomDataSection.js.map +2 -2
  17. package/dist/backend/detail/InlineEditors.js +3 -1
  18. package/dist/backend/detail/InlineEditors.js.map +2 -2
  19. package/dist/backend/detail/TagsSection.js +3 -1
  20. package/dist/backend/detail/TagsSection.js.map +2 -2
  21. package/dist/backend/injection/InjectionSpot.js +10 -8
  22. package/dist/backend/injection/InjectionSpot.js.map +2 -2
  23. package/dist/backend/injection/eventBridge.js +3 -1
  24. package/dist/backend/injection/eventBridge.js.map +2 -2
  25. package/dist/backend/injection/useGuardedMutation.js +3 -1
  26. package/dist/backend/injection/useGuardedMutation.js.map +2 -2
  27. package/dist/backend/injection/useInjectionDataWidgets.js +3 -1
  28. package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
  29. package/dist/backend/injection/useRegisteredComponent.js +6 -9
  30. package/dist/backend/injection/useRegisteredComponent.js.map +2 -2
  31. package/dist/backend/injection/widgetRegistry.js +3 -1
  32. package/dist/backend/injection/widgetRegistry.js.map +2 -2
  33. package/dist/backend/inputs/LookupSelect.js +3 -1
  34. package/dist/backend/inputs/LookupSelect.js.map +2 -2
  35. package/dist/backend/notifications/NotificationDispatcher.js +4 -2
  36. package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
  37. package/dist/backend/sidebar/SidebarCustomizationEditor.js +7 -5
  38. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  39. package/dist/backend/utils/api.js +3 -1
  40. package/dist/backend/utils/api.js.map +2 -2
  41. package/dist/backend/utils/bulkDelete.js +4 -2
  42. package/dist/backend/utils/bulkDelete.js.map +2 -2
  43. package/dist/backend/version-history/VersionHistoryPanel.js +4 -2
  44. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  45. package/dist/portal/hooks/usePortalDashboardWidgets.js +3 -1
  46. package/dist/portal/hooks/usePortalDashboardWidgets.js.map +2 -2
  47. package/dist/portal/hooks/usePortalEventBridge.js +3 -1
  48. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  49. package/dist/primitives/Notice.js +3 -3
  50. package/dist/primitives/Notice.js.map +2 -2
  51. package/dist/theme/ThemeProvider.js +4 -2
  52. package/dist/theme/ThemeProvider.js.map +2 -2
  53. package/package.json +3 -3
  54. package/src/ai/AiChat.tsx +4 -4
  55. package/src/backend/CrudForm.tsx +20 -17
  56. package/src/backend/DataTable.tsx +7 -10
  57. package/src/backend/JsonDisplay.tsx +4 -1
  58. package/src/backend/__tests__/component-replacement.test.tsx +1 -1
  59. package/src/backend/confirm-dialog/useConfirmDialog.tsx +4 -3
  60. package/src/backend/dashboard/DashboardScreen.tsx +9 -6
  61. package/src/backend/dashboard/widgetRegistry.ts +4 -1
  62. package/src/backend/detail/CustomDataSection.tsx +5 -2
  63. package/src/backend/detail/InlineEditors.tsx +4 -1
  64. package/src/backend/detail/TagsSection.tsx +4 -1
  65. package/src/backend/injection/InjectionSpot.tsx +11 -8
  66. package/src/backend/injection/eventBridge.ts +4 -1
  67. package/src/backend/injection/useGuardedMutation.ts +4 -1
  68. package/src/backend/injection/useInjectionDataWidgets.ts +4 -1
  69. package/src/backend/injection/useRegisteredComponent.tsx +7 -9
  70. package/src/backend/injection/widgetRegistry.ts +4 -1
  71. package/src/backend/inputs/LookupSelect.tsx +4 -1
  72. package/src/backend/notifications/NotificationDispatcher.ts +5 -2
  73. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +8 -5
  74. package/src/backend/utils/__tests__/bulkDelete.test.ts +29 -23
  75. package/src/backend/utils/api.ts +4 -2
  76. package/src/backend/utils/bulkDelete.ts +5 -2
  77. package/src/backend/version-history/VersionHistoryPanel.tsx +5 -2
  78. package/src/portal/hooks/usePortalDashboardWidgets.ts +4 -1
  79. package/src/portal/hooks/usePortalEventBridge.ts +4 -1
  80. package/src/primitives/Notice.tsx +4 -4
  81. package/src/theme/ThemeProvider.tsx +5 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/utils/api.ts"],
4
- "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nfunction formatForbiddenAccessMessage(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }): string {\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n if (features.length) {\n return `Access denied: you are missing the required permission \"${features.join(', ')}\". Contact your administrator.`\n }\n if (effectiveRoles.length) {\n return `Access denied: this area requires the role \"${effectiveRoles.join(', ')}\". Contact your administrator.`\n }\n return 'Access denied: you do not have permission to perform this action.'\n}\n\n/**\n * Signal a forbidden access attempt for an authenticated user via a flash banner.\n *\n * Authenticated 403 responses must never redirect to `/login` \u2014 that creates an\n * infinite loop because the login page detects the active session and bounces\n * the user back to the failing destination (see GH #2070). Pages that need an\n * inline banner should catch `ForbiddenError` and render `AccessDeniedMessage`\n * from `@open-mercato/ui/backend/detail`.\n */\nexport function notifyForbiddenAccess(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // Portal routes have their own customer auth \u2014 keep the existing no-op contract.\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash(formatForbiddenAccessMessage(options), 'warning')\n } catch {\n // no-op\n }\n}\n\n/**\n * @deprecated Renamed to {@link notifyForbiddenAccess}. The previous name\n * implied a `/login` redirect that no longer happens (see GH #2070). Kept as an\n * exported alias for one minor version so third-party module imports keep\n * building; update imports to `notifyForbiddenAccess`.\n */\nexport const redirectToForbiddenLogin = notifyForbiddenAccess\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'apiFetch' })\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nfunction formatForbiddenAccessMessage(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }): string {\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const effectiveRoles = roles.length ? roles : DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n if (features.length) {\n return `Access denied: you are missing the required permission \"${features.join(', ')}\". Contact your administrator.`\n }\n if (effectiveRoles.length) {\n return `Access denied: this area requires the role \"${effectiveRoles.join(', ')}\". Contact your administrator.`\n }\n return 'Access denied: you do not have permission to perform this action.'\n}\n\n/**\n * Signal a forbidden access attempt for an authenticated user via a flash banner.\n *\n * Authenticated 403 responses must never redirect to `/login` \u2014 that creates an\n * infinite loop because the login page detects the active session and bounces\n * the user back to the failing destination (see GH #2070). Pages that need an\n * inline banner should catch `ForbiddenError` and render `AccessDeniedMessage`\n * from `@open-mercato/ui/backend/detail`.\n */\nexport function notifyForbiddenAccess(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n // Portal routes have their own customer auth \u2014 keep the existing no-op contract.\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash(formatForbiddenAccessMessage(options), 'warning')\n } catch {\n // no-op\n }\n}\n\n/**\n * @deprecated Renamed to {@link notifyForbiddenAccess}. The previous name\n * implied a `/login` redirect that no longer happens (see GH #2070). Kept as an\n * exported alias for one minor version so third-party module imports keep\n * building; update imports to `notifyForbiddenAccess`.\n */\nexport const redirectToForbiddenLogin = notifyForbiddenAccess\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n logger.warn('Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n notifyForbiddenAccess({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
5
+ "mappings": ";AAGA,SAAS,aAAa;AACtB,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACxC,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAEjE,MAAM,gBAAgB,wBAAwB;AAE9C,SAAS,aAAa,MAA+B,QAAyC;AAC5F,QAAM,UAAU,IAAI,QAAQ,QAAQ,CAAC,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,QAAQ,IAAI,GAAG,EAAG;AACtB,YAAQ,IAAI,KAAK,KAAK;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,SAAkB,YAA6B;AAC3E,SAAO,QAAQ,IAAI,UAAU,MAAM;AACrC;AAEA,SAAS,oBAAoB,OAAmC;AAC9D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,OAAO,OAAO,UAAU;AAC9B,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACJ,MAAI,OAAO,UAAU,SAAU,aAAY;AAAA,WAClC,iBAAiB,IAAK,aAAY,MAAM,SAAS;AAAA,WACjD,OAAO,YAAY,eAAe,iBAAiB,QAAS,aAAY,MAAM;AAAA,MAClF,QAAO;AACZ,MAAI,CAAC,uBAAuB,KAAK,SAAS,EAAG,QAAO;AACpD,MAAI;AACF,WAAO,IAAI,IAAI,SAAS,EAAE,SAAS;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,qBAAwB,SAAiC,KAAmC;AAChH,SAAO,cAAc,kBAAkB,SAAS,GAAG;AACrD;AAEO,MAAM,0BAA0B,MAAM;AAAA,EAE3C,YAAY,UAAU,gBAAgB;AACpC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,2BAA2B;AACzC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAE3D,MAAI,OAAO,SAAS,SAAS,WAAW,WAAW,EAAG;AAEtD,MAAI,wBAAwB,KAAK,OAAO,SAAS,QAAQ,EAAG;AAC5D,MAAI;AACF,UAAM,iDAA4C,SAAS;AAC3D,eAAW,MAAM;AACf,aAAO,SAAS,OAAO,sCAAsC,mBAAmB,OAAO,CAAC;AAAA,IAC1F,GAAG,EAAE;AAAA,EACP,QAAQ;AAAA,EAER;AACF;AAEO,MAAM,uBAAuB,MAAM;AAAA,EAExC,YAAY,UAAU,aAAa;AACjC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,0BAAoC,CAAC,OAAO;AAEzC,SAAS,sBAAsB,KAAoD;AACxF,MAAI,KAAK,yBAAyB,IAAI,sBAAsB,QAAQ;AAClE,8BAA0B,CAAC,GAAG,IAAI,qBAAqB,EAAE,IAAI,MAAM;AAAA,EACrE;AACF;AAEA,SAAS,6BAA6B,SAA2F;AAC/H,QAAM,WAAW,SAAS,kBAAkB,OAAO,OAAO,KAAK,CAAC;AAChE,QAAM,QAAQ,SAAS,eAAe,OAAO,OAAO,KAAK,CAAC;AAC1D,QAAM,iBAAiB,MAAM,SAAS,QAAQ,wBAAwB,OAAO,OAAO;AACpF,MAAI,SAAS,QAAQ;AACnB,WAAO,2DAA2D,SAAS,KAAK,IAAI,CAAC;AAAA,EACvF;AACA,MAAI,eAAe,QAAQ;AACzB,WAAO,+CAA+C,eAAe,KAAK,IAAI,CAAC;AAAA,EACjF;AACA,SAAO;AACT;AAWO,SAAS,sBAAsB,SAAmF;AACvH,MAAI,OAAO,WAAW,YAAa;AAEnC,MAAI,wBAAwB,KAAK,OAAO,SAAS,QAAQ,EAAG;AAC5D,MAAI;AACF,UAAM,6BAA6B,OAAO,GAAG,SAAS;AAAA,EACxD,QAAQ;AAAA,EAER;AACF;AAQO,MAAM,2BAA2B;AAExC,eAAsB,SAAS,OAA0B,MAAuC;AAE9F,QAAM,gBACJ,OAAO,WAAW,cACb,OAAsD,oBACvD;AACN,QAAM,gBAAiB,WAAyD;AAChF,QAAM,YAAY,iBAAiB;AACnC,MAAI,CAAC,WAAW;AACd,WAAO,IAAI;AAAA,MACT,KAAK,UAAU,EAAE,OAAO,6CAA6C,CAAC;AAAA,MACtE,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,mBAAmB,EAAE;AAAA,IACjE;AAAA,EACF;AACA,QAAM,SAAS,cAAc,qBAAqB;AAClD,QAAM,WAAwB,OAAO,KAAK,MAAM,EAAE,SAC9C,EAAE,GAAI,QAAQ,CAAC,GAAI,SAAS,aAAa,MAAM,SAAS,MAAM,EAAE,IAChE,QAAQ,CAAC;AAIb,QAAM,aAA0B,SAAS,cACrC,WACA,oBAAoB,KAAK,IACvB,EAAE,GAAG,UAAU,aAAa,UAAU,IACtC;AACN,QAAM,iBAAiB,IAAI,QAAQ,YAAY,OAAO;AACtD,QAAM,8BAA8B,qBAAqB,gBAAgB,4BAA4B;AACrG,QAAM,2BAA2B,qBAAqB,gBAAgB,yBAAyB;AAC/F,QAAM,MAAM,MAAM,UAAU,OAAO,UAAU;AAC7C,QAAM,WAAW,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;AAC5E,QAAM,cAAc,SAAS,WAAW,QAAQ;AAChD,QAAM,gBAAgB,wBAAwB,KAAK,QAAQ;AAC3D,MAAI,IAAI,WAAW,KAAK;AAGtB,QAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,6BAA6B;AAClE,+BAAyB;AAEzB,YAAM,IAAI,kBAAkB,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,cAAc,CAAC;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AACA,MAAI,IAAI,WAAW,KAAK;AAEtB,QAAI,QAAyB;AAC7B,QAAI,WAA4B;AAChC,QAAI,UAAmB;AACvB,UAAM,UAAU,MAAM,aAAsC,IAAI,MAAM,GAAG,IAAI;AAC7E,QAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,UAAI,MAAM,QAAQ,QAAQ,aAAa,GAAG;AACxC,gBAAQ,QAAQ,cAAc,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;AAAA,MACpD;AACA,UAAI,MAAM,QAAQ,QAAQ,gBAAgB,GAAG;AAC3C,mBAAW,QAAQ,iBAAiB,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC;AAAA,MAC1D;AACA,gBAAU;AAAA,IACZ;AAEA,QAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,0BAA0B;AAC/D,YAAM,SACJ,OAAO,UAAU,WACb,QACA,iBAAiB,MACf,MAAM,SAAS,IACd,OAAO,YAAY,eAAe,iBAAiB,UAClD,MAAM,MACN;AACV,UAAI;AACF,eAAO,KAAK,sBAAsB;AAAA,UAChC,KAAK;AAAA,UACL,QAAQ,IAAI;AAAA,UACZ,eAAe;AAAA,UACf,kBAAkB;AAAA,UAClB,SAAS;AAAA,QACX,CAAC;AAAA,MACH,QAAQ;AAAA,MAAC;AACT,YAAM,cAAc,QAAS,SAAS,MAAM,UAAY,YAAY,SAAS,MAAO;AACpF,UAAI,aAAa;AACf,8BAAsB,EAAE,eAAe,OAAO,kBAAkB,SAAS,CAAC;AAAA,MAC5E;AACA,UAAI,MAAM;AACV,UAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,YAAI,OAAO,QAAQ,UAAU,UAAU;AACrC,gBAAM,QAAQ;AAAA,QAChB,WAAW,OAAO,QAAQ,YAAY,UAAU;AAC9C,gBAAM,QAAQ;AAAA,QAChB;AAAA,MACF,OAAO;AACL,cAAM,MAAM,IAAI,MAAM,EAAE,KAAK,EAAE,MAAM,MAAM,WAAW;AAAA,MACxD;AACA,YAAM,IAAI,eAAe,GAAG;AAAA,IAC9B;AAAA,EAEF;AACA,MAAI;AACF,UAAM,SAAS,IAAI,QAAQ,IAAI,gBAAgB;AAC/C,UAAM,WAAW,6BAA6B,MAAM;AACpD,QAAI,SAAU,eAAc,QAAQ;AAAA,EACtC,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,aAAa,IAAI,QAAQ,IAAI,oBAAoB;AACvD,QAAI,YAAY;AACd,YAAM,SAAS,KAAK,MAAM,UAAU;AACpC,UAAI,UAAU,OAAO,WAAW,YAAY,OAAO,SAAS,iBAAiB;AAC3E,cAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,OAAO,OAAO,UAAU,EAAE;AAC7F,YAAI,QAAQ;AACV,gBAAM,YAAY,OAAO,OAAO,cAAc,WAAW,OAAO,YAAY;AAC5E,gBAAM,eAAe,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AACrF,gBAAM,QAAQ,OAAO,UAAU,WAAW,WAAW;AACrD,gBAAM,cACJ,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,IAC9D,OAAO,YAAY,KAAK,IACxB;AACN,kCAAwB,EAAE,QAAQ,aAAa,WAAW,cAAc,MAAM,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  import { emitProgressUpdate } from "@open-mercato/shared/lib/frontend/progressEvents";
3
+ import { createLogger } from "@open-mercato/shared/lib/logger";
4
+ const logger = createLogger("ui");
3
5
  function createClientProgressJobId(jobType) {
4
6
  const cryptoRef = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
5
7
  if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
@@ -50,8 +52,8 @@ async function runBulkDelete(rows, deleteOne, options) {
50
52
  const rawCode = err && typeof err === "object" ? err.code : void 0;
51
53
  const code = typeof rawCode === "string" && rawCode.length > 0 ? rawCode : null;
52
54
  failures.push({ id: row.id, code, message });
53
- if (logTag && typeof console !== "undefined" && typeof console.warn === "function") {
54
- console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err });
55
+ if (logTag) {
56
+ logger.warn("Bulk delete failed", { logTag, id: row.id, code, message, err });
55
57
  }
56
58
  }
57
59
  if (progress && progressJobId) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/utils/bulkDelete.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport { emitProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'\n\nexport type BulkDeleteFailure = {\n id: string\n code: string | null\n message: string\n}\n\nexport type BulkDeleteOutcome<T extends { id: string }> = {\n succeeded: T[]\n failures: BulkDeleteFailure[]\n}\n\nexport type BulkDeleteOptions = {\n fallbackErrorMessage?: string\n logTag?: string\n progress?: {\n jobId?: string\n jobType: string\n name: string\n description?: string | null\n meta?: Record<string, unknown> | null\n }\n}\n\nfunction createClientProgressJobId(jobType: string): string {\n const cryptoRef =\n typeof globalThis !== 'undefined'\n ? (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n : undefined\n if (cryptoRef && typeof cryptoRef.randomUUID === 'function') {\n return `client:${cryptoRef.randomUUID()}`\n }\n return `client:${jobType}:${Date.now()}:${Math.random().toString(36).slice(2)}`\n}\n\nfunction calculateProgress(processed: number, total: number): number {\n if (total <= 0) return 100\n return Math.max(0, Math.min(100, Math.round((processed / total) * 100)))\n}\n\nfunction calculateEtaSeconds(startedAtMs: number, processed: number, total: number): number | null {\n if (processed <= 0 || processed >= total) return null\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1000))\n return Math.ceil((elapsedSeconds / processed) * (total - processed))\n}\n\nexport async function runBulkDelete<T extends { id: string }>(\n rows: T[],\n deleteOne: (row: T) => Promise<void>,\n options?: BulkDeleteOptions,\n): Promise<BulkDeleteOutcome<T>> {\n const fallback = options?.fallbackErrorMessage ?? 'Delete failed'\n const logTag = options?.logTag\n const succeeded: T[] = []\n const failures: BulkDeleteFailure[] = []\n const progress = options?.progress\n const progressJobId = progress && rows.length > 0 ? progress.jobId ?? createClientProgressJobId(progress.jobType) : null\n const startedAtMs = Date.now()\n const startedAt = new Date(startedAtMs).toISOString()\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: 0,\n processedCount: 0,\n totalCount: rows.length,\n etaSeconds: null,\n cancellable: false,\n startedAt,\n })\n }\n for (const row of rows) {\n try {\n await deleteOne(row)\n succeeded.push(row)\n } catch (err) {\n const message =\n err instanceof Error && err.message.trim().length > 0 ? err.message : fallback\n const rawCode =\n err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const code = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : null\n failures.push({ id: row.id, code, message })\n if (logTag && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err })\n }\n }\n if (progress && progressJobId) {\n const processed = succeeded.length + failures.length\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: calculateProgress(processed, rows.length),\n processedCount: processed,\n totalCount: rows.length,\n etaSeconds: calculateEtaSeconds(startedAtMs, processed, rows.length),\n cancellable: false,\n startedAt,\n })\n }\n }\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: {\n ...(progress.meta ?? {}),\n succeededCount: succeeded.length,\n failedCount: failures.length,\n },\n status: failures.length === rows.length && rows.length > 0 ? 'failed' : 'completed',\n progressPercent: 100,\n processedCount: rows.length,\n totalCount: rows.length,\n etaSeconds: 0,\n cancellable: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n errorMessage:\n failures.length === rows.length && failures[0]?.message\n ? failures[0].message\n : null,\n })\n }\n return { succeeded, failures }\n}\n\nexport type BulkDeleteFailureGroup = {\n key: string\n count: number\n sampleMessage: string\n ids: string[]\n}\n\n// Keyed by `code` when present so semantically identical failures collapse into\n// one toast even if their message text varies (e.g. row-specific dependency\n// counts). Falls back to the raw message when the server did not surface a code.\nexport function groupBulkDeleteFailures(\n failures: BulkDeleteFailure[],\n): BulkDeleteFailureGroup[] {\n const buckets = new Map<string, BulkDeleteFailureGroup>()\n for (const failure of failures) {\n const key = failure.code || failure.message || 'unknown'\n const existing = buckets.get(key)\n if (existing) {\n existing.count += 1\n existing.ids.push(failure.id)\n continue\n }\n buckets.set(key, {\n key,\n count: 1,\n sampleMessage: failure.message,\n ids: [failure.id],\n })\n }\n return Array.from(buckets.values())\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport { emitProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type BulkDeleteFailure = {\n id: string\n code: string | null\n message: string\n}\n\nexport type BulkDeleteOutcome<T extends { id: string }> = {\n succeeded: T[]\n failures: BulkDeleteFailure[]\n}\n\nexport type BulkDeleteOptions = {\n fallbackErrorMessage?: string\n logTag?: string\n progress?: {\n jobId?: string\n jobType: string\n name: string\n description?: string | null\n meta?: Record<string, unknown> | null\n }\n}\n\nfunction createClientProgressJobId(jobType: string): string {\n const cryptoRef =\n typeof globalThis !== 'undefined'\n ? (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n : undefined\n if (cryptoRef && typeof cryptoRef.randomUUID === 'function') {\n return `client:${cryptoRef.randomUUID()}`\n }\n return `client:${jobType}:${Date.now()}:${Math.random().toString(36).slice(2)}`\n}\n\nfunction calculateProgress(processed: number, total: number): number {\n if (total <= 0) return 100\n return Math.max(0, Math.min(100, Math.round((processed / total) * 100)))\n}\n\nfunction calculateEtaSeconds(startedAtMs: number, processed: number, total: number): number | null {\n if (processed <= 0 || processed >= total) return null\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1000))\n return Math.ceil((elapsedSeconds / processed) * (total - processed))\n}\n\nexport async function runBulkDelete<T extends { id: string }>(\n rows: T[],\n deleteOne: (row: T) => Promise<void>,\n options?: BulkDeleteOptions,\n): Promise<BulkDeleteOutcome<T>> {\n const fallback = options?.fallbackErrorMessage ?? 'Delete failed'\n const logTag = options?.logTag\n const succeeded: T[] = []\n const failures: BulkDeleteFailure[] = []\n const progress = options?.progress\n const progressJobId = progress && rows.length > 0 ? progress.jobId ?? createClientProgressJobId(progress.jobType) : null\n const startedAtMs = Date.now()\n const startedAt = new Date(startedAtMs).toISOString()\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: 0,\n processedCount: 0,\n totalCount: rows.length,\n etaSeconds: null,\n cancellable: false,\n startedAt,\n })\n }\n for (const row of rows) {\n try {\n await deleteOne(row)\n succeeded.push(row)\n } catch (err) {\n const message =\n err instanceof Error && err.message.trim().length > 0 ? err.message : fallback\n const rawCode =\n err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const code = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : null\n failures.push({ id: row.id, code, message })\n if (logTag) {\n logger.warn('Bulk delete failed', { logTag, id: row.id, code, message, err })\n }\n }\n if (progress && progressJobId) {\n const processed = succeeded.length + failures.length\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: calculateProgress(processed, rows.length),\n processedCount: processed,\n totalCount: rows.length,\n etaSeconds: calculateEtaSeconds(startedAtMs, processed, rows.length),\n cancellable: false,\n startedAt,\n })\n }\n }\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: {\n ...(progress.meta ?? {}),\n succeededCount: succeeded.length,\n failedCount: failures.length,\n },\n status: failures.length === rows.length && rows.length > 0 ? 'failed' : 'completed',\n progressPercent: 100,\n processedCount: rows.length,\n totalCount: rows.length,\n etaSeconds: 0,\n cancellable: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n errorMessage:\n failures.length === rows.length && failures[0]?.message\n ? failures[0].message\n : null,\n })\n }\n return { succeeded, failures }\n}\n\nexport type BulkDeleteFailureGroup = {\n key: string\n count: number\n sampleMessage: string\n ids: string[]\n}\n\n// Keyed by `code` when present so semantically identical failures collapse into\n// one toast even if their message text varies (e.g. row-specific dependency\n// counts). Falls back to the raw message when the server did not surface a code.\nexport function groupBulkDeleteFailures(\n failures: BulkDeleteFailure[],\n): BulkDeleteFailureGroup[] {\n const buckets = new Map<string, BulkDeleteFailureGroup>()\n for (const failure of failures) {\n const key = failure.code || failure.message || 'unknown'\n const existing = buckets.get(key)\n if (existing) {\n existing.count += 1\n existing.ids.push(failure.id)\n continue\n }\n buckets.set(key, {\n key,\n count: 1,\n sampleMessage: failure.message,\n ids: [failure.id],\n })\n }\n return Array.from(buckets.values())\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,6 +11,8 @@ import { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from "./la
11
11
  import { useAuditPermissions, canUndoEntry, canRedoEntry } from "./useAuditPermissions.js";
12
12
  import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
13
13
  import { humanizeResourceKind } from "./labels.js";
14
+ import { createLogger } from "@open-mercato/shared/lib/logger";
15
+ const logger = createLogger("ui");
14
16
  function VersionHistoryPanel({
15
17
  open,
16
18
  onOpenChange,
@@ -83,7 +85,7 @@ function VersionHistoryPanel({
83
85
  window.location.reload();
84
86
  }
85
87
  } catch (err) {
86
- console.error(t("audit_logs.actions.undo"), err);
88
+ logger.error("Undo failed", { err });
87
89
  } finally {
88
90
  setUndoingToken(null);
89
91
  }
@@ -102,7 +104,7 @@ function VersionHistoryPanel({
102
104
  window.location.reload();
103
105
  }
104
106
  } catch (err) {
105
- console.error(t("audit_logs.actions.redo"), err);
107
+ logger.error("Redo failed", { err });
106
108
  } finally {
107
109
  setRedoingId(null);
108
110
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/version-history/VersionHistoryPanel.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.undo'), err)\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n console.error(t('audit_logs.actions.redo'), err)\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert variant=\"info\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ChevronLeft, Clock, Loader2, RotateCcw, Undo2, X } from 'lucide-react'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport type { VersionHistoryEntry } from './types'\nimport { VersionHistoryDetail } from './VersionHistoryDetail'\nimport { formatDate } from '@open-mercato/core/modules/audit_logs/lib/display-helpers'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { markRedoConsumed, markUndoSuccess } from '@open-mercato/ui/backend/operations/store'\nimport { getVersionHistoryActionLabel, getVersionHistoryStatusLabel } from './labels'\nimport { useAuditPermissions, canUndoEntry, canRedoEntry } from './useAuditPermissions'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { humanizeResourceKind } from './labels'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\nexport type VersionHistoryPanelProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n entries: VersionHistoryEntry[]\n isLoading: boolean\n error: string | null\n hasMore: boolean\n onLoadMore: () => void\n t: TranslateFn\n /** Explicit override \u2014 when provided, skips auto-check and uses this value directly. */\n canUndoRedo?: boolean\n /** When true (default), auto-checks audit_logs features for the current user. Ignored when canUndoRedo is provided. */\n autoCheckAcl?: boolean\n}\n\nexport function VersionHistoryPanel({\n open,\n onOpenChange,\n entries,\n isLoading,\n error,\n hasMore,\n onLoadMore,\n t,\n canUndoRedo,\n autoCheckAcl = true,\n}: VersionHistoryPanelProps) {\n const shouldAutoCheck = canUndoRedo === undefined && autoCheckAcl\n const permissions = useAuditPermissions(shouldAutoCheck && open)\n\n const visibleEntries = React.useMemo(() => {\n if (canUndoRedo !== undefined || !shouldAutoCheck) return entries\n if (permissions.isLoading) return entries\n if (permissions.canViewTenant) return entries\n if (!permissions.currentUserId) return entries\n return entries.filter((entry) => entry.actorUserId === permissions.currentUserId)\n }, [entries, canUndoRedo, shouldAutoCheck, permissions])\n\n const [selectedEntry, setSelectedEntry] = React.useState<VersionHistoryEntry | null>(null)\n const [undoingToken, setUndoingToken] = React.useState<string | null>(null)\n const [redoingId, setRedoingId] = React.useState<string | null>(null)\n const latestUndoableId = React.useMemo(() => {\n const latest = visibleEntries.find((entry) => entry.undoToken && entry.executionState === 'done')\n return latest?.id ?? null\n }, [visibleEntries])\n const latestUndoneId = React.useMemo(() => {\n const undone = visibleEntries.filter((entry) => entry.executionState === 'undone')\n if (!undone.length) return null\n const sorted = [...undone].sort((a, b) => {\n const aTs = Date.parse(a.updatedAt)\n const bTs = Date.parse(b.updatedAt)\n return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0)\n })\n return sorted[0]?.id ?? null\n }, [visibleEntries])\n\n React.useEffect(() => {\n if (!open) setSelectedEntry(null)\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [open])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && open) {\n onOpenChange(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [open, onOpenChange])\n\n const handleUndo = React.useCallback(async (token: string | null) => {\n if (!token) return\n setUndoingToken(token)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/undo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ undoToken: token }),\n }, { errorMessage: t('audit_logs.error.undo') })\n markUndoSuccess(token)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Undo failed', { err })\n } finally {\n setUndoingToken(null)\n }\n }, [t])\n\n const handleRedo = React.useCallback(async (logId: string | null) => {\n if (!logId) return\n setRedoingId(logId)\n try {\n await apiCallOrThrow('/api/audit_logs/audit-logs/actions/redo', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ logId }),\n }, { errorMessage: t('audit_logs.error.redo') })\n markRedoConsumed(logId)\n if (typeof window !== 'undefined') {\n window.location.reload()\n }\n } catch (err) {\n logger.error('Redo failed', { err })\n } finally {\n setRedoingId(null)\n }\n }, [t])\n\n if (!open) return null\n\n const isEmpty = visibleEntries.length === 0 && !isLoading && !error\n const isInitialLoading = visibleEntries.length === 0 && isLoading\n const isInitialError = visibleEntries.length === 0 && !!error\n\n return (\n <>\n <div\n className=\"fixed inset-0 z-overlay bg-black/20\"\n onClick={() => onOpenChange(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-modal h-full w-full max-w-md border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('audit_logs.version_history.title')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n {selectedEntry ? (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setSelectedEntry(null)}\n aria-label={t('audit_logs.version_history.detail.back')}\n >\n <ChevronLeft className=\"h-5 w-5\" />\n </Button>\n ) : (\n <Clock className=\"h-5 w-5\" />\n )}\n <h2 className=\"font-semibold\">\n {selectedEntry\n ? t('audit_logs.version_history.detail.title')\n : t('audit_logs.version_history.title')}\n </h2>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => onOpenChange(false)}\n aria-label={t('audit_logs.version_history.close')}\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n {selectedEntry ? (\n <VersionHistoryDetail entry={selectedEntry} t={t} />\n ) : (\n <div className=\"space-y-3\">\n {shouldAutoCheck && !permissions.isLoading && !permissions.canViewTenant && permissions.currentUserId ? (\n <Alert variant=\"info\">\n <AlertDescription>\n {t('audit_logs.hint.view_self_only', 'Showing only your own changes. Contact an administrator for broader access.')}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {isInitialLoading ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Loader2 className=\"mb-2 h-8 w-8 animate-spin\" />\n <p>{t('audit_logs.version_history.loading')}</p>\n </div>\n ) : null}\n\n {isInitialError ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground\">\n <p>{t('audit_logs.version_history.error')}</p>\n <Button variant=\"ghost\" size=\"sm\" onClick={onLoadMore}>\n {t('audit_logs.common.refresh')}\n </Button>\n </div>\n ) : null}\n\n {isEmpty ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground\">\n <Clock className=\"mb-2 h-8 w-8 opacity-50\" />\n <p>{t('audit_logs.version_history.empty')}</p>\n </div>\n ) : null}\n\n {visibleEntries.length > 0 ? (\n <div className=\"divide-y rounded-lg border\">\n {visibleEntries.map((entry) => {\n const statusLabel = getVersionHistoryStatusLabel(entry.executionState, t)\n const actionLabel = getVersionHistoryActionLabel(entry, t)\n const isRelatedEntry = entry.parentResourceKind != null\n const entryCanUndo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canUndoEntry(permissions, entry.actorUserId) : true)\n const entryCanRedo = canUndoRedo !== undefined\n ? canUndoRedo\n : (shouldAutoCheck ? canRedoEntry(permissions, entry.actorUserId) : true)\n const canUndo = entryCanUndo\n && Boolean(entry.undoToken)\n && entry.executionState === 'done'\n && entry.id === latestUndoableId\n const showRedo = entryCanRedo && entry.executionState === 'undone'\n const canRedo = showRedo && entry.id === latestUndoneId\n return (\n <div\n key={entry.id}\n className={`flex items-start justify-between gap-3 py-3 transition-colors hover:bg-muted/50 ${isRelatedEntry ? 'pl-8 pr-4 border-l-2 border-l-muted-foreground/20' : 'px-4'}`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"h-auto min-w-0 flex-1 flex-col items-start justify-start gap-1 whitespace-normal px-0 py-0 text-left hover:bg-transparent\"\n onClick={() => setSelectedEntry(entry)}\n >\n {isRelatedEntry ? (\n <span className=\"text-overline uppercase tracking-wider text-muted-foreground/70 font-medium\">\n {humanizeResourceKind(entry.resourceKind, t)}\n </span>\n ) : null}\n <div className=\"break-words text-sm font-medium\">\n {actionLabel}\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n <span>{entry.actorUserName || entry.actorUserId || t('audit_logs.common.none')}</span>\n <span>\u2022</span>\n <span>{formatDate(entry.createdAt)}</span>\n <span>\u2022</span>\n <span>{statusLabel}</span>\n </div>\n </Button>\n <div className=\"flex items-center gap-1 pt-1\">\n {canUndo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.undo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleUndo(entry.undoToken)\n }}\n disabled={undoingToken === entry.undoToken || Boolean(redoingId)}\n >\n <Undo2 className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.undo')}</span>\n </Button>\n ) : null}\n {showRedo ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1 px-2 text-xs\"\n aria-label={t('audit_logs.actions.redo')}\n onClick={(event) => {\n event.stopPropagation()\n void handleRedo(entry.id)\n }}\n disabled={!canRedo || redoingId === entry.id || Boolean(undoingToken)}\n >\n <RotateCcw className=\"size-4\" aria-hidden=\"true\" />\n <span>{t('audit_logs.actions.redo')}</span>\n </Button>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : null}\n\n {error && visibleEntries.length > 0 ? (\n <div className=\"text-xs text-red-500\">{error}</div>\n ) : null}\n\n {hasMore ? (\n <div className=\"pt-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onLoadMore}\n disabled={isLoading}\n >\n {isLoading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : null}\n {t('audit_logs.version_history.load_more')}\n </Button>\n </div>\n ) : null}\n </div>\n )}\n </div>\n </div>\n </div>\n </>\n )\n}\n"],
5
+ "mappings": ";AAiJI,mBACE,KAaM,YAdR;AA/IJ,YAAY,WAAW;AACvB,SAAS,aAAa,OAAO,SAAS,WAAW,OAAO,SAAS;AAEjE,SAAS,cAAc;AAEvB,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB,uBAAuB;AAClD,SAAS,8BAA8B,oCAAoC;AAC3E,SAAS,qBAAqB,cAAc,oBAAoB;AAChE,SAAS,OAAO,wBAAwB;AACxC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI;AAiBzB,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAA6B;AAC3B,QAAM,kBAAkB,gBAAgB,UAAa;AACrD,QAAM,cAAc,oBAAoB,mBAAmB,IAAI;AAE/D,QAAM,iBAAiB,MAAM,QAAQ,MAAM;AACzC,QAAI,gBAAgB,UAAa,CAAC,gBAAiB,QAAO;AAC1D,QAAI,YAAY,UAAW,QAAO;AAClC,QAAI,YAAY,cAAe,QAAO;AACtC,QAAI,CAAC,YAAY,cAAe,QAAO;AACvC,WAAO,QAAQ,OAAO,CAAC,UAAU,MAAM,gBAAgB,YAAY,aAAa;AAAA,EAClF,GAAG,CAAC,SAAS,aAAa,iBAAiB,WAAW,CAAC;AAEvD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAqC,IAAI;AACzF,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,IAAI;AAC1E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,mBAAmB,MAAM,QAAQ,MAAM;AAC3C,UAAM,SAAS,eAAe,KAAK,CAAC,UAAU,MAAM,aAAa,MAAM,mBAAmB,MAAM;AAChG,WAAO,QAAQ,MAAM;AAAA,EACvB,GAAG,CAAC,cAAc,CAAC;AACnB,QAAM,iBAAiB,MAAM,QAAQ,MAAM;AACzC,UAAM,SAAS,eAAe,OAAO,CAAC,UAAU,MAAM,mBAAmB,QAAQ;AACjF,QAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,UAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM;AACxC,YAAM,MAAM,KAAK,MAAM,EAAE,SAAS;AAClC,YAAM,MAAM,KAAK,MAAM,EAAE,SAAS;AAClC,cAAQ,OAAO,SAAS,GAAG,IAAI,MAAM,MAAM,OAAO,SAAS,GAAG,IAAI,MAAM;AAAA,IAC1E,CAAC;AACD,WAAO,OAAO,CAAC,GAAG,MAAM;AAAA,EAC1B,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM,kBAAiB,IAAI;AAAA,EAClC,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,SAAS,KAAK,MAAM;AACjC,aAAS,KAAK,MAAM,WAAW;AAC/B,WAAO,MAAM;AACX,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,UAAU,MAAM;AACpB,UAAM,gBAAgB,CAAC,UAAyB;AAC9C,UAAI,MAAM,QAAQ,YAAY,MAAM;AAClC,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,aAAa;AAClD,WAAO,MAAM,SAAS,oBAAoB,WAAW,aAAa;AAAA,EACpE,GAAG,CAAC,MAAM,YAAY,CAAC;AAEvB,QAAM,aAAa,MAAM,YAAY,OAAO,UAAyB;AACnE,QAAI,CAAC,MAAO;AACZ,oBAAgB,KAAK;AACrB,QAAI;AACF,YAAM,eAAe,2CAA2C;AAAA,QAC9D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,WAAW,MAAM,CAAC;AAAA,MAC3C,GAAG,EAAE,cAAc,EAAE,uBAAuB,EAAE,CAAC;AAC/C,sBAAgB,KAAK;AACrB,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,eAAe,EAAE,IAAI,CAAC;AAAA,IACrC,UAAE;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,aAAa,MAAM,YAAY,OAAO,UAAyB;AACnE,QAAI,CAAC,MAAO;AACZ,iBAAa,KAAK;AAClB,QAAI;AACF,YAAM,eAAe,2CAA2C;AAAA,QAC9D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,GAAG,EAAE,cAAc,EAAE,uBAAuB,EAAE,CAAC;AAC/C,uBAAiB,KAAK;AACtB,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,eAAe,EAAE,IAAI,CAAC;AAAA,IACrC,UAAE;AACA,mBAAa,IAAI;AAAA,IACnB;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,UAAU,eAAe,WAAW,KAAK,CAAC,aAAa,CAAC;AAC9D,QAAM,mBAAmB,eAAe,WAAW,KAAK;AACxD,QAAM,iBAAiB,eAAe,WAAW,KAAK,CAAC,CAAC;AAExD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,SAAS,MAAM,aAAa,KAAK;AAAA,QACjC,eAAY;AAAA;AAAA,IACd;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QACX,cAAY,EAAE,kCAAkC;AAAA,QAEhD,+BAAC,SAAI,WAAU,wBACb;AAAA,+BAAC,SAAI,WAAU,wDACb;AAAA,iCAAC,SAAI,WAAU,2BACZ;AAAA,8BACC;AAAA,gBAAC;AAAA;AAAA,kBACC,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,SAAS,MAAM,iBAAiB,IAAI;AAAA,kBACpC,cAAY,EAAE,wCAAwC;AAAA,kBAEtD,8BAAC,eAAY,WAAU,WAAU;AAAA;AAAA,cACnC,IAEA,oBAAC,SAAM,WAAU,WAAU;AAAA,cAE7B,oBAAC,QAAG,WAAU,iBACX,0BACG,EAAE,yCAAyC,IAC3C,EAAE,kCAAkC,GAC1C;AAAA,eACF;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,SAAS,MAAM,aAAa,KAAK;AAAA,gBACjC,cAAY,EAAE,kCAAkC;AAAA,gBAEhD,8BAAC,KAAE,WAAU,WAAU;AAAA;AAAA,YACzB;AAAA,aACF;AAAA,UAEA,oBAAC,SAAI,WAAU,oCACZ,0BACC,oBAAC,wBAAqB,OAAO,eAAe,GAAM,IAElD,qBAAC,SAAI,WAAU,aACZ;AAAA,+BAAmB,CAAC,YAAY,aAAa,CAAC,YAAY,iBAAiB,YAAY,gBACtF,oBAAC,SAAM,SAAQ,QACb,8BAAC,oBACE,YAAE,kCAAkC,6EAA6E,GACpH,GACF,IACE;AAAA,YAEH,mBACC,qBAAC,SAAI,WAAU,yEACb;AAAA,kCAAC,WAAQ,WAAU,6BAA4B;AAAA,cAC/C,oBAAC,OAAG,YAAE,oCAAoC,GAAE;AAAA,eAC9C,IACE;AAAA,YAEH,iBACC,qBAAC,SAAI,WAAU,+EACb;AAAA,kCAAC,OAAG,YAAE,kCAAkC,GAAE;AAAA,cAC1C,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,SAAS,YACxC,YAAE,2BAA2B,GAChC;AAAA,eACF,IACE;AAAA,YAEH,UACC,qBAAC,SAAI,WAAU,yEACb;AAAA,kCAAC,SAAM,WAAU,2BAA0B;AAAA,cAC3C,oBAAC,OAAG,YAAE,kCAAkC,GAAE;AAAA,eAC5C,IACE;AAAA,YAEH,eAAe,SAAS,IACvB,oBAAC,SAAI,WAAU,8BACZ,yBAAe,IAAI,CAAC,UAAU;AAC7B,oBAAM,cAAc,6BAA6B,MAAM,gBAAgB,CAAC;AACxE,oBAAM,cAAc,6BAA6B,OAAO,CAAC;AACzD,oBAAM,iBAAiB,MAAM,sBAAsB;AACnD,oBAAM,eAAe,gBAAgB,SACjC,cACC,kBAAkB,aAAa,aAAa,MAAM,WAAW,IAAI;AACtE,oBAAM,eAAe,gBAAgB,SACjC,cACC,kBAAkB,aAAa,aAAa,MAAM,WAAW,IAAI;AACtE,oBAAM,UAAU,gBACX,QAAQ,MAAM,SAAS,KACvB,MAAM,mBAAmB,UACzB,MAAM,OAAO;AAClB,oBAAM,WAAW,gBAAgB,MAAM,mBAAmB;AAC1D,oBAAM,UAAU,YAAY,MAAM,OAAO;AACzC,qBACE;AAAA,gBAAC;AAAA;AAAA,kBAEC,WAAW,mFAAmF,iBAAiB,sDAAsD,MAAM;AAAA,kBAE3K;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,MAAK;AAAA,wBACL,SAAQ;AAAA,wBACR,WAAU;AAAA,wBACV,SAAS,MAAM,iBAAiB,KAAK;AAAA,wBAEpC;AAAA,2CACC,oBAAC,UAAK,WAAU,+EACb,+BAAqB,MAAM,cAAc,CAAC,GAC7C,IACE;AAAA,0BACJ,oBAAC,SAAI,WAAU,mCACZ,uBACH;AAAA,0BACA,qBAAC,SAAI,WAAU,6EACb;AAAA,gDAAC,UAAM,gBAAM,iBAAiB,MAAM,eAAe,EAAE,wBAAwB,GAAE;AAAA,4BAC/E,oBAAC,UAAK,oBAAC;AAAA,4BACP,oBAAC,UAAM,qBAAW,MAAM,SAAS,GAAE;AAAA,4BACnC,oBAAC,UAAK,oBAAC;AAAA,4BACP,oBAAC,UAAM,uBAAY;AAAA,6BACrB;AAAA;AAAA;AAAA,oBACF;AAAA,oBACA,qBAAC,SAAI,WAAU,gCACZ;AAAA,gCACC;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAQ;AAAA,0BACR,MAAK;AAAA,0BACL,WAAU;AAAA,0BACV,cAAY,EAAE,yBAAyB;AAAA,0BACvC,SAAS,CAAC,UAAU;AAClB,kCAAM,gBAAgB;AACtB,iCAAK,WAAW,MAAM,SAAS;AAAA,0BACjC;AAAA,0BACA,UAAU,iBAAiB,MAAM,aAAa,QAAQ,SAAS;AAAA,0BAE/D;AAAA,gDAAC,SAAM,WAAU,UAAS,eAAY,QAAO;AAAA,4BAC7C,oBAAC,UAAM,YAAE,yBAAyB,GAAE;AAAA;AAAA;AAAA,sBACtC,IACE;AAAA,sBACH,WACC;AAAA,wBAAC;AAAA;AAAA,0BACC,SAAQ;AAAA,0BACR,MAAK;AAAA,0BACL,WAAU;AAAA,0BACV,cAAY,EAAE,yBAAyB;AAAA,0BACvC,SAAS,CAAC,UAAU;AAClB,kCAAM,gBAAgB;AACtB,iCAAK,WAAW,MAAM,EAAE;AAAA,0BAC1B;AAAA,0BACA,UAAU,CAAC,WAAW,cAAc,MAAM,MAAM,QAAQ,YAAY;AAAA,0BAEpE;AAAA,gDAAC,aAAU,WAAU,UAAS,eAAY,QAAO;AAAA,4BACjD,oBAAC,UAAM,YAAE,yBAAyB,GAAE;AAAA;AAAA;AAAA,sBACtC,IACE;AAAA,uBACN;AAAA;AAAA;AAAA,gBA1DK,MAAM;AAAA,cA2Db;AAAA,YAEJ,CAAC,GACH,IACE;AAAA,YAEH,SAAS,eAAe,SAAS,IAChC,oBAAC,SAAI,WAAU,wBAAwB,iBAAM,IAC3C;AAAA,YAEH,UACC,oBAAC,SAAI,WAAU,QACb;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,SAAS;AAAA,gBACT,UAAU;AAAA,gBAET;AAAA,8BACC,oBAAC,WAAQ,WAAU,6BAA4B,IAC7C;AAAA,kBACH,EAAE,sCAAsC;AAAA;AAAA;AAAA,YAC3C,GACF,IACE;AAAA,aACN,GAEJ;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ import * as React from "react";
3
3
  import { loadInjectionWidgetsForSpot } from "@open-mercato/shared/modules/widgets/injection-loader";
4
4
  import { hasAllFeatures } from "@open-mercato/shared/security/features";
5
5
  import { apiCall } from "../../backend/utils/apiCall.js";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ const logger = createLogger("ui").child({ component: "usePortalDashboardWidgets" });
6
8
  function collectRequiredFeatures(widgets) {
7
9
  const set = /* @__PURE__ */ new Set();
8
10
  for (const widget of widgets) {
@@ -48,7 +50,7 @@ function usePortalDashboardWidgets(spotId) {
48
50
  setGrantedFeatures(granted);
49
51
  } catch (loadError) {
50
52
  if (!mounted) return;
51
- console.error(`[usePortalDashboardWidgets] Failed to load widgets for spot ${spotId}:`, loadError);
53
+ logger.error("Failed to load widgets for spot", { spotId, err: loadError });
52
54
  setError(loadError instanceof Error ? loadError.message : String(loadError));
53
55
  setWidgets([]);
54
56
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/portal/hooks/usePortalDashboardWidgets.ts"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { apiCall } from '../../backend/utils/apiCall'\n\ntype PortalFeatureCheckResponse = {\n ok: boolean\n granted?: string[]\n}\n\nfunction collectRequiredFeatures(widgets: LoadedInjectionWidget[]): string[] {\n const set = new Set<string>()\n for (const widget of widgets) {\n for (const feature of widget.metadata.features ?? []) {\n if (!feature || feature.trim().length === 0) continue\n set.add(feature)\n }\n }\n return Array.from(set)\n}\n\nasync function readPortalGrantedFeatures(features: string[]): Promise<Set<string>> {\n if (features.length === 0) return new Set()\n try {\n const { ok, result: data } = await apiCall<PortalFeatureCheckResponse>('/api/customer_accounts/portal/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features }),\n })\n if (!ok || !data?.ok) return new Set()\n return new Set(data.granted ?? [])\n } catch {\n return new Set()\n }\n}\n\n/**\n * Loads UI injection widgets (with Widget component) for a portal spot.\n *\n * Unlike `useInjectionDataWidgets` which loads data-only widgets (columns, fields, menuItems),\n * this hook loads widgets that export a `Widget` React component \u2014 suitable for\n * portal dashboard sections and other UI injection spots.\n *\n * Feature gating: widgets declaring `metadata.features` are filtered against the\n * authenticated customer's grants resolved via\n * `/api/customer_accounts/portal/feature-check`. Wildcard grants (`portal.*`) resolve\n * through the shared matcher.\n */\nexport function usePortalDashboardWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [grantedFeatures, setGrantedFeatures] = React.useState<Set<string>>(new Set())\n\n React.useEffect(() => {\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n // Only keep widgets that have a Widget component\n const uiWidgets = loaded.filter((w) => typeof w.Widget === 'function')\n setWidgets(uiWidgets)\n const required = collectRequiredFeatures(uiWidgets)\n const granted = await readPortalGrantedFeatures(required)\n if (!mounted) return\n setGrantedFeatures(granted)\n } catch (loadError) {\n if (!mounted) return\n console.error(`[usePortalDashboardWidgets] Failed to load widgets for spot ${spotId}:`, loadError)\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId])\n\n const grantedFeatureList = React.useMemo(() => Array.from(grantedFeatures), [grantedFeatures])\n\n const visibleWidgets = React.useMemo(\n () =>\n widgets.filter((widget) => {\n const required = widget.metadata.features ?? []\n return required.length === 0 || hasAllFeatures(grantedFeatureList, required)\n }),\n [widgets, grantedFeatureList],\n )\n\n return { widgets: visibleWidgets, isLoading, error }\n}\n"],
5
- "mappings": ";AAEA,YAAY,WAAW;AAEvB,SAAS,mCAA+D;AACxE,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAOxB,SAAS,wBAAwB,SAA4C;AAC3E,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,UAAU,SAAS;AAC5B,eAAW,WAAW,OAAO,SAAS,YAAY,CAAC,GAAG;AACpD,UAAI,CAAC,WAAW,QAAQ,KAAK,EAAE,WAAW,EAAG;AAC7C,UAAI,IAAI,OAAO;AAAA,IACjB;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEA,eAAe,0BAA0B,UAA0C;AACjF,MAAI,SAAS,WAAW,EAAG,QAAO,oBAAI,IAAI;AAC1C,MAAI;AACF,UAAM,EAAE,IAAI,QAAQ,KAAK,IAAI,MAAM,QAAoC,+CAA+C;AAAA,MACpH,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;AAAA,IACnC,CAAC;AACD,QAAI,CAAC,MAAM,CAAC,MAAM,GAAI,QAAO,oBAAI,IAAI;AACrC,WAAO,IAAI,IAAI,KAAK,WAAW,CAAC,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO,oBAAI,IAAI;AAAA,EACjB;AACF;AAcO,SAAS,0BAA0B,QAIxC;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAkC,CAAC,CAAC;AACxE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAsB,oBAAI,IAAI,CAAC;AAEnF,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACd,UAAM,OAAO,YAAY;AACvB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,IAAI;AACb,cAAM,SAAS,MAAM,4BAA4B,MAAM;AACvD,YAAI,CAAC,QAAS;AAEd,cAAM,YAAY,OAAO,OAAO,CAAC,MAAM,OAAO,EAAE,WAAW,UAAU;AACrE,mBAAW,SAAS;AACpB,cAAM,WAAW,wBAAwB,SAAS;AAClD,cAAM,UAAU,MAAM,0BAA0B,QAAQ;AACxD,YAAI,CAAC,QAAS;AACd,2BAAmB,OAAO;AAAA,MAC5B,SAAS,WAAW;AAClB,YAAI,CAAC,QAAS;AACd,gBAAQ,MAAM,+DAA+D,MAAM,KAAK,SAAS;AACjG,iBAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,CAAC;AAC3E,mBAAW,CAAC,CAAC;AAAA,MACf,UAAE;AACA,YAAI,QAAS,cAAa,KAAK;AAAA,MACjC;AAAA,IACF;AACA,SAAK,KAAK;AACV,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,qBAAqB,MAAM,QAAQ,MAAM,MAAM,KAAK,eAAe,GAAG,CAAC,eAAe,CAAC;AAE7F,QAAM,iBAAiB,MAAM;AAAA,IAC3B,MACE,QAAQ,OAAO,CAAC,WAAW;AACzB,YAAM,WAAW,OAAO,SAAS,YAAY,CAAC;AAC9C,aAAO,SAAS,WAAW,KAAK,eAAe,oBAAoB,QAAQ;AAAA,IAC7E,CAAC;AAAA,IACH,CAAC,SAAS,kBAAkB;AAAA,EAC9B;AAEA,SAAO,EAAE,SAAS,gBAAgB,WAAW,MAAM;AACrD;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { apiCall } from '../../backend/utils/apiCall'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'usePortalDashboardWidgets' })\n\ntype PortalFeatureCheckResponse = {\n ok: boolean\n granted?: string[]\n}\n\nfunction collectRequiredFeatures(widgets: LoadedInjectionWidget[]): string[] {\n const set = new Set<string>()\n for (const widget of widgets) {\n for (const feature of widget.metadata.features ?? []) {\n if (!feature || feature.trim().length === 0) continue\n set.add(feature)\n }\n }\n return Array.from(set)\n}\n\nasync function readPortalGrantedFeatures(features: string[]): Promise<Set<string>> {\n if (features.length === 0) return new Set()\n try {\n const { ok, result: data } = await apiCall<PortalFeatureCheckResponse>('/api/customer_accounts/portal/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features }),\n })\n if (!ok || !data?.ok) return new Set()\n return new Set(data.granted ?? [])\n } catch {\n return new Set()\n }\n}\n\n/**\n * Loads UI injection widgets (with Widget component) for a portal spot.\n *\n * Unlike `useInjectionDataWidgets` which loads data-only widgets (columns, fields, menuItems),\n * this hook loads widgets that export a `Widget` React component \u2014 suitable for\n * portal dashboard sections and other UI injection spots.\n *\n * Feature gating: widgets declaring `metadata.features` are filtered against the\n * authenticated customer's grants resolved via\n * `/api/customer_accounts/portal/feature-check`. Wildcard grants (`portal.*`) resolve\n * through the shared matcher.\n */\nexport function usePortalDashboardWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [grantedFeatures, setGrantedFeatures] = React.useState<Set<string>>(new Set())\n\n React.useEffect(() => {\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n // Only keep widgets that have a Widget component\n const uiWidgets = loaded.filter((w) => typeof w.Widget === 'function')\n setWidgets(uiWidgets)\n const required = collectRequiredFeatures(uiWidgets)\n const granted = await readPortalGrantedFeatures(required)\n if (!mounted) return\n setGrantedFeatures(granted)\n } catch (loadError) {\n if (!mounted) return\n logger.error('Failed to load widgets for spot', { spotId, err: loadError })\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId])\n\n const grantedFeatureList = React.useMemo(() => Array.from(grantedFeatures), [grantedFeatures])\n\n const visibleWidgets = React.useMemo(\n () =>\n widgets.filter((widget) => {\n const required = widget.metadata.features ?? []\n return required.length === 0 || hasAllFeatures(grantedFeatureList, required)\n }),\n [widgets, grantedFeatureList],\n )\n\n return { widgets: visibleWidgets, isLoading, error }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AAEvB,SAAS,mCAA+D;AACxE,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,4BAA4B,CAAC;AAOlF,SAAS,wBAAwB,SAA4C;AAC3E,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,UAAU,SAAS;AAC5B,eAAW,WAAW,OAAO,SAAS,YAAY,CAAC,GAAG;AACpD,UAAI,CAAC,WAAW,QAAQ,KAAK,EAAE,WAAW,EAAG;AAC7C,UAAI,IAAI,OAAO;AAAA,IACjB;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEA,eAAe,0BAA0B,UAA0C;AACjF,MAAI,SAAS,WAAW,EAAG,QAAO,oBAAI,IAAI;AAC1C,MAAI;AACF,UAAM,EAAE,IAAI,QAAQ,KAAK,IAAI,MAAM,QAAoC,+CAA+C;AAAA,MACpH,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;AAAA,IACnC,CAAC;AACD,QAAI,CAAC,MAAM,CAAC,MAAM,GAAI,QAAO,oBAAI,IAAI;AACrC,WAAO,IAAI,IAAI,KAAK,WAAW,CAAC,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO,oBAAI,IAAI;AAAA,EACjB;AACF;AAcO,SAAS,0BAA0B,QAIxC;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAkC,CAAC,CAAC;AACxE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAsB,oBAAI,IAAI,CAAC;AAEnF,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACd,UAAM,OAAO,YAAY;AACvB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,IAAI;AACb,cAAM,SAAS,MAAM,4BAA4B,MAAM;AACvD,YAAI,CAAC,QAAS;AAEd,cAAM,YAAY,OAAO,OAAO,CAAC,MAAM,OAAO,EAAE,WAAW,UAAU;AACrE,mBAAW,SAAS;AACpB,cAAM,WAAW,wBAAwB,SAAS;AAClD,cAAM,UAAU,MAAM,0BAA0B,QAAQ;AACxD,YAAI,CAAC,QAAS;AACd,2BAAmB,OAAO;AAAA,MAC5B,SAAS,WAAW;AAClB,YAAI,CAAC,QAAS;AACd,eAAO,MAAM,mCAAmC,EAAE,QAAQ,KAAK,UAAU,CAAC;AAC1E,iBAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,CAAC;AAC3E,mBAAW,CAAC,CAAC;AAAA,MACf,UAAE;AACA,YAAI,QAAS,cAAa,KAAK;AAAA,MACjC;AAAA,IACF;AACA,SAAK,KAAK;AACV,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,qBAAqB,MAAM,QAAQ,MAAM,MAAM,KAAK,eAAe,GAAG,CAAC,eAAe,CAAC;AAE7F,QAAM,iBAAiB,MAAM;AAAA,IAC3B,MACE,QAAQ,OAAO,CAAC,WAAW;AACzB,YAAM,WAAW,OAAO,SAAS,YAAY,CAAC;AAC9C,aAAO,SAAS,WAAW,KAAK,eAAe,oBAAoB,QAAQ;AAAA,IAC7E,CAAC;AAAA,IACH,CAAC,SAAS,kBAAkB;AAAA,EAC9B;AAEA,SAAO,EAAE,SAAS,gBAAgB,WAAW,MAAM;AACrD;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { PORTAL_EVENT_DOM_NAME } from "./usePortalAppEvent.js";
4
+ import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ const logger = createLogger("ui").child({ component: "PortalEventBridge" });
4
6
  const PORTAL_SSE_ENDPOINT = "/api/customer_accounts/portal/events/stream";
5
7
  const HEARTBEAT_TIMEOUT = 45e3;
6
8
  const RECONNECT_BASE_MS = 1e3;
@@ -33,7 +35,7 @@ function usePortalEventBridge() {
33
35
  function resetHeartbeatTimer() {
34
36
  if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current);
35
37
  heartbeatTimer.current = setTimeout(() => {
36
- console.warn("[PortalEventBridge] Heartbeat timeout \u2014 reconnecting");
38
+ logger.warn("Heartbeat timeout \u2014 reconnecting");
37
39
  disconnect();
38
40
  scheduleReconnect();
39
41
  }, HEARTBEAT_TIMEOUT);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/portal/hooks/usePortalEventBridge.ts"],
4
- "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): void {\n const sourceRef = useRef<EventSource | null>(null)\n const reconnectAttempts = useRef(0)\n const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const heartbeatTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const recentEvents = useRef<Map<string, number>>(new Map())\n const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\n\n useEffect(() => {\n let mounted = true\n\n function isDuplicate(eventPayload: AppEventPayload): boolean {\n const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`\n const lastSeen = recentEvents.current.get(key)\n if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true\n recentEvents.current.set(key, Date.now())\n if (recentEvents.current.size > 100) {\n const now = Date.now()\n for (const [k, v] of recentEvents.current) {\n if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k)\n }\n }\n return false\n }\n\n function resetHeartbeatTimer() {\n if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = setTimeout(() => {\n console.warn('[PortalEventBridge] Heartbeat timeout \u2014 reconnecting')\n disconnect()\n scheduleReconnect()\n }, HEARTBEAT_TIMEOUT)\n }\n\n function connect() {\n if (!mounted) return\n if (sourceRef.current) return\n\n try {\n const source = new EventSource(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\n }\n\n source.onmessage = (event) => {\n resetHeartbeatTimer()\n if (!event.data || event.data === ':heartbeat') return\n\n try {\n const parsed = JSON.parse(event.data) as AppEventPayload\n if (!parsed.id || typeof parsed.id !== 'string') return\n if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n if (mounted) scheduleReconnect()\n }\n }\n\n function disconnect() {\n if (sourceRef.current) {\n sourceRef.current.close()\n sourceRef.current = null\n }\n if (heartbeatTimer.current) {\n clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = null\n }\n }\n\n function scheduleReconnect() {\n if (reconnectTimer.current) return\n const delay = Math.min(\n RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),\n RECONNECT_MAX_MS,\n )\n reconnectAttempts.current++\n reconnectTimer.current = setTimeout(() => {\n reconnectTimer.current = null\n connect()\n }, delay)\n }\n\n connect()\n\n return () => {\n mounted = false\n disconnect()\n if (reconnectTimer.current) {\n clearTimeout(reconnectTimer.current)\n reconnectTimer.current = null\n }\n }\n }, [])\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { PORTAL_EVENT_DOM_NAME } from './usePortalAppEvent'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'PortalEventBridge' })\n\nconst PORTAL_SSE_ENDPOINT = '/api/customer_accounts/portal/events/stream'\nconst HEARTBEAT_TIMEOUT = 45_000\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\nconst DEDUP_WINDOW_MS = 500\nconst PORTAL_BRIDGE_RECONNECTED_EVENT_ID = 'om:portal-bridge:reconnected'\n\n/**\n * React hook that establishes a singleton SSE connection to the portal event bridge.\n *\n * Mount once in the portal shell/layout. Receives server-side events with\n * `portalBroadcast: true` and dispatches them as `om:portal-event` CustomEvents\n * on the window object for consumption by `usePortalAppEvent`.\n *\n * Uses customer auth (cookie-based JWT) instead of staff auth.\n *\n * @example\n * ```tsx\n * import { usePortalEventBridge } from '@open-mercato/ui/portal/hooks/usePortalEventBridge'\n *\n * function PortalShell({ children }) {\n * usePortalEventBridge()\n * return <div>{children}</div>\n * }\n * ```\n */\nexport function usePortalEventBridge(): void {\n const sourceRef = useRef<EventSource | null>(null)\n const reconnectAttempts = useRef(0)\n const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const heartbeatTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const recentEvents = useRef<Map<string, number>>(new Map())\n const hasEverConnected = useRef(false)\n const reconnectPending = useRef(false)\n\n useEffect(() => {\n let mounted = true\n\n function isDuplicate(eventPayload: AppEventPayload): boolean {\n const key = `${eventPayload.id}:${JSON.stringify(eventPayload.payload ?? {})}`\n const lastSeen = recentEvents.current.get(key)\n if (lastSeen && Date.now() - lastSeen < DEDUP_WINDOW_MS) return true\n recentEvents.current.set(key, Date.now())\n if (recentEvents.current.size > 100) {\n const now = Date.now()\n for (const [k, v] of recentEvents.current) {\n if (now - v > DEDUP_WINDOW_MS * 2) recentEvents.current.delete(k)\n }\n }\n return false\n }\n\n function resetHeartbeatTimer() {\n if (heartbeatTimer.current) clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = setTimeout(() => {\n logger.warn('Heartbeat timeout \u2014 reconnecting')\n disconnect()\n scheduleReconnect()\n }, HEARTBEAT_TIMEOUT)\n }\n\n function connect() {\n if (!mounted) return\n if (sourceRef.current) return\n\n try {\n const source = new EventSource(PORTAL_SSE_ENDPOINT, { withCredentials: true })\n sourceRef.current = source\n\n source.onopen = () => {\n const shouldEmitReconnect = hasEverConnected.current && reconnectPending.current\n hasEverConnected.current = true\n reconnectPending.current = false\n reconnectAttempts.current = 0\n resetHeartbeatTimer()\n if (shouldEmitReconnect) {\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, {\n detail: {\n id: PORTAL_BRIDGE_RECONNECTED_EVENT_ID,\n payload: {},\n timestamp: Date.now(),\n organizationId: '',\n } satisfies AppEventPayload,\n }),\n )\n }\n }\n\n source.onmessage = (event) => {\n resetHeartbeatTimer()\n if (!event.data || event.data === ':heartbeat') return\n\n try {\n const parsed = JSON.parse(event.data) as AppEventPayload\n if (!parsed.id || typeof parsed.id !== 'string') return\n if (isDuplicate(parsed)) return\n\n window.dispatchEvent(\n new CustomEvent(PORTAL_EVENT_DOM_NAME, { detail: parsed }),\n )\n } catch {\n // Ignore malformed events\n }\n }\n\n source.onerror = () => {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n disconnect()\n if (mounted) scheduleReconnect()\n }\n } catch {\n if (hasEverConnected.current) {\n reconnectPending.current = true\n }\n if (mounted) scheduleReconnect()\n }\n }\n\n function disconnect() {\n if (sourceRef.current) {\n sourceRef.current.close()\n sourceRef.current = null\n }\n if (heartbeatTimer.current) {\n clearTimeout(heartbeatTimer.current)\n heartbeatTimer.current = null\n }\n }\n\n function scheduleReconnect() {\n if (reconnectTimer.current) return\n const delay = Math.min(\n RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts.current),\n RECONNECT_MAX_MS,\n )\n reconnectAttempts.current++\n reconnectTimer.current = setTimeout(() => {\n reconnectTimer.current = null\n connect()\n }, delay)\n }\n\n connect()\n\n return () => {\n mounted = false\n disconnect()\n if (reconnectTimer.current) {\n clearTimeout(reconnectTimer.current)\n reconnectTimer.current = null\n }\n }\n }, [])\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "@open-mercato/shared/lib/utils";
4
+ import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ const logger = createLogger("ui");
4
6
  const variantStyles = {
5
7
  error: {
6
8
  container: "border-status-error-border bg-status-error-bg text-status-error-text",
@@ -25,9 +27,7 @@ function Notice({
25
27
  compact = false
26
28
  }) {
27
29
  if (process.env.NODE_ENV === "development") {
28
- console.warn(
29
- '[DS] <Notice> is deprecated. Use <Alert variant="destructive|warning|info"> instead. See: docs/design-system/migration-tables.md'
30
- );
30
+ logger.warn('<Notice> is deprecated. Use <Alert variant="destructive|warning|info"> instead. See: docs/design-system/migration-tables.md');
31
31
  }
32
32
  const styles = variantStyles[variant];
33
33
  if (compact || !title && !action && (children || message)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/Notice.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * @deprecated Use <Alert variant=\"destructive|warning|info\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert variant=\"destructive\"\n * Notice variant=\"warning\" \u2192 Alert variant=\"warning\"\n * Notice variant=\"info\" \u2192 Alert variant=\"info\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n '[DS] <Notice> is deprecated. Use <Alert variant=\"destructive|warning|info\"> instead. ' +\n 'See: docs/design-system/migration-tables.md'\n )\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": ";AA4DM,cAaE,YAbF;AA1DN,SAAS,UAAU;AAWnB,MAAM,gBAAgB;AAAA,EACpB,OAAO;AAAA,IACL,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AACF;AAcO,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAgB;AACd,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,SAAS,cAAc,OAAO;AAEpC,MAAI,WAAY,CAAC,SAAS,CAAC,WAAW,YAAY,UAAW;AAC3D,WACE,oBAAC,SAAI,WAAW,GAAG,uCAAuC,OAAO,WAAW,SAAS,GAClF,sBAAY,SACf;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,yBAAyB,OAAO,WAAW,SAAS,GACrE,+BAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,8DAA8D,OAAO,IAAI;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IACA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,oBAAC,SAAI,WAAU,uBAAuB,iBAAM,IAAS;AAAA,MAC7D,UAAU,oBAAC,SAAI,WAAU,sBAAsB,mBAAQ,IAAS;AAAA,MAChE;AAAA,MACA,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,OACnD;AAAA,KACF,GACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui')\n\n/**\n * @deprecated Use <Alert variant=\"destructive|warning|info\"> instead.\n * Migration guide: docs/design-system/migration-tables.md#j3-component-mapping\n *\n * Notice variant=\"error\" \u2192 Alert variant=\"destructive\"\n * Notice variant=\"warning\" \u2192 Alert variant=\"warning\"\n * Notice variant=\"info\" \u2192 Alert variant=\"info\"\n */\n\nconst variantStyles = {\n error: {\n container: 'border-status-error-border bg-status-error-bg text-status-error-text',\n icon: 'border-status-error-icon',\n },\n info: {\n container: 'border-status-info-border bg-status-info-bg text-status-info-text',\n icon: 'border-status-info-icon',\n },\n warning: {\n container: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n icon: 'border-status-warning-icon',\n },\n} as const\n\nexport type NoticeVariant = keyof typeof variantStyles\n\nexport type NoticeProps = {\n variant?: NoticeVariant\n title?: string\n message?: string\n action?: React.ReactNode\n children?: React.ReactNode\n className?: string\n compact?: boolean\n}\n\nexport function Notice({\n variant = 'info',\n title,\n message,\n action,\n children,\n className,\n compact = false,\n}: NoticeProps) {\n if (process.env.NODE_ENV === 'development') {\n logger.warn('<Notice> is deprecated. Use <Alert variant=\"destructive|warning|info\"> instead. See: docs/design-system/migration-tables.md')\n }\n\n const styles = variantStyles[variant]\n\n if (compact || (!title && !action && (children || message))) {\n return (\n <div className={cn('rounded-md border px-3 py-2 text-sm', styles.container, className)}>\n {children ?? message}\n </div>\n )\n }\n\n return (\n <div className={cn('rounded-md border p-4', styles.container, className)}>\n <div className=\"flex items-start gap-3\">\n <span\n className={cn('inline-block mt-0.5 h-4 w-4 rounded-full border-2 shrink-0', styles.icon)}\n aria-hidden\n />\n <div className=\"space-y-1\">\n {title ? <div className=\"text-sm font-medium\">{title}</div> : null}\n {message ? <div className=\"text-sm opacity-90\">{message}</div> : null}\n {children}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": ";AA4DM,cAaE,YAbF;AA1DN,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,IAAI;AAWhC,MAAM,gBAAgB;AAAA,EACpB,OAAO;AAAA,IACL,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM;AAAA,EACR;AACF;AAcO,SAAS,OAAO;AAAA,EACrB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAAgB;AACd,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,WAAO,KAAK,6HAA6H;AAAA,EAC3I;AAEA,QAAM,SAAS,cAAc,OAAO;AAEpC,MAAI,WAAY,CAAC,SAAS,CAAC,WAAW,YAAY,UAAW;AAC3D,WACE,oBAAC,SAAI,WAAW,GAAG,uCAAuC,OAAO,WAAW,SAAS,GAClF,sBAAY,SACf;AAAA,EAEJ;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,yBAAyB,OAAO,WAAW,SAAS,GACrE,+BAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,8DAA8D,OAAO,IAAI;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IACA,qBAAC,SAAI,WAAU,aACZ;AAAA,cAAQ,oBAAC,SAAI,WAAU,uBAAuB,iBAAM,IAAS;AAAA,MAC7D,UAAU,oBAAC,SAAI,WAAU,sBAAsB,mBAAQ,IAAS;AAAA,MAChE;AAAA,MACA,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,OACnD;AAAA,KACF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,8 @@
2
2
  import { Fragment, jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { createContext, useContext } from "react";
5
+ import { createLogger } from "@open-mercato/shared/lib/logger";
6
+ const logger = createLogger("ui").child({ component: "ThemeProvider" });
5
7
  const ThemeContext = createContext(void 0);
6
8
  const THEME_STORAGE_KEY = "om-theme";
7
9
  function getSystemTheme() {
@@ -17,7 +19,7 @@ function getStoredTheme() {
17
19
  }
18
20
  } catch (error) {
19
21
  if (process.env.NODE_ENV === "development") {
20
- console.warn("[ThemeProvider] localStorage read failed:", error);
22
+ logger.warn("localStorage read failed", { err: error });
21
23
  }
22
24
  }
23
25
  return "system";
@@ -61,7 +63,7 @@ function ThemeProvider({ children }) {
61
63
  localStorage.setItem(THEME_STORAGE_KEY, newTheme);
62
64
  } catch (error) {
63
65
  if (process.env.NODE_ENV === "development") {
64
- console.warn("[ThemeProvider] localStorage write failed:", error);
66
+ logger.warn("localStorage write failed", { err: error });
65
67
  }
66
68
  }
67
69
  const resolved = newTheme === "system" ? getSystemTheme() : newTheme;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/theme/ThemeProvider.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\n\nexport type Theme = 'light' | 'dark' | 'system'\n\ntype ThemeContextValue = {\n theme: Theme\n resolvedTheme: 'light' | 'dark'\n setTheme: (theme: Theme) => void\n}\n\nconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined)\n\nconst THEME_STORAGE_KEY = 'om-theme'\n\nfunction getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nfunction getStoredTheme(): Theme {\n if (typeof window === 'undefined') return 'system'\n try {\n const stored = localStorage.getItem(THEME_STORAGE_KEY)\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n return stored\n }\n } catch (error) {\n // localStorage may be unavailable in private browsing, iframes, or restricted contexts\n // Theme will default to system preference - this is expected graceful degradation\n if (process.env.NODE_ENV === 'development') {\n console.warn('[ThemeProvider] localStorage read failed:', error)\n }\n }\n return 'system'\n}\n\nfunction applyTheme(resolvedTheme: 'light' | 'dark') {\n const root = document.documentElement\n if (resolvedTheme === 'dark') {\n root.classList.add('dark')\n } else {\n root.classList.remove('dark')\n }\n}\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n const [theme, setThemeState] = React.useState<Theme>('system')\n const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>('light')\n const [mounted, setMounted] = React.useState(false)\n\n // Initialize theme from localStorage on mount\n React.useEffect(() => {\n const stored = getStoredTheme()\n setThemeState(stored)\n const resolved = stored === 'system' ? getSystemTheme() : stored\n setResolvedTheme(resolved)\n applyTheme(resolved)\n setMounted(true)\n }, [])\n\n // Listen for system theme changes\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n const handleChange = () => {\n if (theme === 'system') {\n const newResolved = getSystemTheme()\n setResolvedTheme(newResolved)\n applyTheme(newResolved)\n }\n }\n\n mediaQuery.addEventListener('change', handleChange)\n return () => mediaQuery.removeEventListener('change', handleChange)\n }, [theme])\n\n const setTheme = React.useCallback((newTheme: Theme) => {\n setThemeState(newTheme)\n try {\n localStorage.setItem(THEME_STORAGE_KEY, newTheme)\n } catch (error) {\n // localStorage may be unavailable - theme still works for this session, just won't persist\n if (process.env.NODE_ENV === 'development') {\n console.warn('[ThemeProvider] localStorage write failed:', error)\n }\n }\n const resolved = newTheme === 'system' ? getSystemTheme() : newTheme\n setResolvedTheme(resolved)\n applyTheme(resolved)\n }, [])\n\n const value = React.useMemo(\n () => ({ theme, resolvedTheme, setTheme }),\n [theme, resolvedTheme, setTheme]\n )\n\n // Prevent flash of wrong theme during hydration\n if (!mounted) {\n return <>{children}</>\n }\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = useContext(ThemeContext)\n if (context === undefined) {\n // Return safe defaults when not in provider (e.g., server render)\n return {\n theme: 'system',\n resolvedTheme: 'light',\n setTheme: () => {},\n }\n }\n return context\n}\n"],
5
- "mappings": ";AAsGW;AApGX,YAAY,WAAW;AACvB,SAAS,eAAe,kBAAkB;AAU1C,MAAM,eAAe,cAA6C,MAAS;AAE3E,MAAM,oBAAoB;AAE1B,SAAS,iBAAmC;AAC1C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO,OAAO,WAAW,8BAA8B,EAAE,UAAU,SAAS;AAC9E;AAEA,SAAS,iBAAwB;AAC/B,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,UAAM,SAAS,aAAa,QAAQ,iBAAiB;AACrD,QAAI,WAAW,WAAW,WAAW,UAAU,WAAW,UAAU;AAClE,aAAO;AAAA,IACT;AAAA,EACF,SAAS,OAAO;AAGd,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,cAAQ,KAAK,6CAA6C,KAAK;AAAA,IACjE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,eAAiC;AACnD,QAAM,OAAO,SAAS;AACtB,MAAI,kBAAkB,QAAQ;AAC5B,SAAK,UAAU,IAAI,MAAM;AAAA,EAC3B,OAAO;AACL,SAAK,UAAU,OAAO,MAAM;AAAA,EAC9B;AACF;AAEO,SAAS,cAAc,EAAE,SAAS,GAAkC;AACzE,QAAM,CAAC,OAAO,aAAa,IAAI,MAAM,SAAgB,QAAQ;AAC7D,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAA2B,OAAO;AAClF,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAGlD,QAAM,UAAU,MAAM;AACpB,UAAM,SAAS,eAAe;AAC9B,kBAAc,MAAM;AACpB,UAAM,WAAW,WAAW,WAAW,eAAe,IAAI;AAC1D,qBAAiB,QAAQ;AACzB,eAAW,QAAQ;AACnB,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAGL,QAAM,UAAU,MAAM;AACpB,QAAI,OAAO,WAAW,YAAa;AAEnC,UAAM,aAAa,OAAO,WAAW,8BAA8B;AACnE,UAAM,eAAe,MAAM;AACzB,UAAI,UAAU,UAAU;AACtB,cAAM,cAAc,eAAe;AACnC,yBAAiB,WAAW;AAC5B,mBAAW,WAAW;AAAA,MACxB;AAAA,IACF;AAEA,eAAW,iBAAiB,UAAU,YAAY;AAClD,WAAO,MAAM,WAAW,oBAAoB,UAAU,YAAY;AAAA,EACpE,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,WAAW,MAAM,YAAY,CAAC,aAAoB;AACtD,kBAAc,QAAQ;AACtB,QAAI;AACF,mBAAa,QAAQ,mBAAmB,QAAQ;AAAA,IAClD,SAAS,OAAO;AAEd,UAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,gBAAQ,KAAK,8CAA8C,KAAK;AAAA,MAClE;AAAA,IACF;AACA,UAAM,WAAW,aAAa,WAAW,eAAe,IAAI;AAC5D,qBAAiB,QAAQ;AACzB,eAAW,QAAQ;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,MAAM;AAAA,IAClB,OAAO,EAAE,OAAO,eAAe,SAAS;AAAA,IACxC,CAAC,OAAO,eAAe,QAAQ;AAAA,EACjC;AAGA,MAAI,CAAC,SAAS;AACZ,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,SAAO,oBAAC,aAAa,UAAb,EAAsB,OAAe,UAAS;AACxD;AAEO,SAAS,WAA8B;AAC5C,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,YAAY,QAAW;AAEzB,WAAO;AAAA,MACL,OAAO;AAAA,MACP,eAAe;AAAA,MACf,UAAU,MAAM;AAAA,MAAC;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('ui').child({ component: 'ThemeProvider' })\n\nexport type Theme = 'light' | 'dark' | 'system'\n\ntype ThemeContextValue = {\n theme: Theme\n resolvedTheme: 'light' | 'dark'\n setTheme: (theme: Theme) => void\n}\n\nconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined)\n\nconst THEME_STORAGE_KEY = 'om-theme'\n\nfunction getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nfunction getStoredTheme(): Theme {\n if (typeof window === 'undefined') return 'system'\n try {\n const stored = localStorage.getItem(THEME_STORAGE_KEY)\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n return stored\n }\n } catch (error) {\n // localStorage may be unavailable in private browsing, iframes, or restricted contexts\n // Theme will default to system preference - this is expected graceful degradation\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage read failed', { err: error })\n }\n }\n return 'system'\n}\n\nfunction applyTheme(resolvedTheme: 'light' | 'dark') {\n const root = document.documentElement\n if (resolvedTheme === 'dark') {\n root.classList.add('dark')\n } else {\n root.classList.remove('dark')\n }\n}\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n const [theme, setThemeState] = React.useState<Theme>('system')\n const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>('light')\n const [mounted, setMounted] = React.useState(false)\n\n // Initialize theme from localStorage on mount\n React.useEffect(() => {\n const stored = getStoredTheme()\n setThemeState(stored)\n const resolved = stored === 'system' ? getSystemTheme() : stored\n setResolvedTheme(resolved)\n applyTheme(resolved)\n setMounted(true)\n }, [])\n\n // Listen for system theme changes\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n const handleChange = () => {\n if (theme === 'system') {\n const newResolved = getSystemTheme()\n setResolvedTheme(newResolved)\n applyTheme(newResolved)\n }\n }\n\n mediaQuery.addEventListener('change', handleChange)\n return () => mediaQuery.removeEventListener('change', handleChange)\n }, [theme])\n\n const setTheme = React.useCallback((newTheme: Theme) => {\n setThemeState(newTheme)\n try {\n localStorage.setItem(THEME_STORAGE_KEY, newTheme)\n } catch (error) {\n // localStorage may be unavailable - theme still works for this session, just won't persist\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage write failed', { err: error })\n }\n }\n const resolved = newTheme === 'system' ? getSystemTheme() : newTheme\n setResolvedTheme(resolved)\n applyTheme(resolved)\n }, [])\n\n const value = React.useMemo(\n () => ({ theme, resolvedTheme, setTheme }),\n [theme, resolvedTheme, setTheme]\n )\n\n // Prevent flash of wrong theme during hydration\n if (!mounted) {\n return <>{children}</>\n }\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = useContext(ThemeContext)\n if (context === undefined) {\n // Return safe defaults when not in provider (e.g., server render)\n return {\n theme: 'system',\n resolvedTheme: 'light',\n setTheme: () => {},\n }\n }\n return context\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.6-develop.6465.1.019f0fb26f",
3
+ "version": "0.6.6-develop.6472.1.1673e7e66b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,13 +155,13 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.6-develop.6465.1.019f0fb26f",
158
+ "@open-mercato/shared": "0.6.6-develop.6472.1.1673e7e66b",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
- "@open-mercato/shared": "0.6.6-develop.6465.1.019f0fb26f",
164
+ "@open-mercato/shared": "0.6.6-develop.6472.1.1673e7e66b",
165
165
  "@testing-library/dom": "^10.4.1",
166
166
  "@testing-library/jest-dom": "^6.9.1",
167
167
  "@testing-library/react": "^16.3.1",
package/src/ai/AiChat.tsx CHANGED
@@ -77,6 +77,9 @@ import { useAiShortcuts } from './useAiShortcuts'
77
77
  import { LoopTracePanel, type LoopTracePanelTrace } from './LoopTracePanel'
78
78
  import { AiIcon } from './AiIcon'
79
79
  import { readModelPickerValue, writeModelPickerValue } from './modelPickerStorage'
80
+ import { createLogger } from '@open-mercato/shared/lib/logger'
81
+
82
+ const logger = createLogger('ui').child({ component: 'AiChat' })
80
83
 
81
84
  // Cap inline previews so we do not blow past localStorage quota (~5MB on most
82
85
  // browsers). Images larger than this still upload + send to the LLM as inline
@@ -936,10 +939,7 @@ function AiUiPartRenderer({
936
939
  if (Component) return
937
940
  if (isReserved) return
938
941
  try {
939
- // eslint-disable-next-line no-console
940
- console.warn(
941
- `[AiChat] No component registered for UI part "${part.componentId}".`,
942
- )
942
+ logger.warn('No component registered for UI part', { componentId: part.componentId })
943
943
  } catch {
944
944
  // noop
945
945
  }