@open-mercato/core 0.6.6-develop.6121.1.5797a901b3 → 0.6.6-develop.6130.1.7e07d10dbb

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.
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
+ import { sql } from "kysely";
3
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
5
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
6
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
@@ -63,21 +64,20 @@ async function GET(req, context) {
63
64
  dscope
64
65
  );
65
66
  const conversationIds = conversations.map((c) => c.id).filter(Boolean);
66
- let links = [];
67
+ const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 };
68
+ let totalsLast24h = 0;
67
69
  let recentFailures = [];
68
70
  if (conversationIds.length > 0) {
69
71
  const since = new Date(Date.now() - WINDOW_HOURS * 60 * 60 * 1e3);
70
- links = await findWithDecryption(
71
- em,
72
- MessageChannelLink,
73
- {
74
- externalConversationId: { $in: conversationIds },
75
- tenantId: auth.tenantId,
76
- createdAt: { $gte: since }
77
- },
78
- void 0,
79
- dscope
80
- );
72
+ const db = em.getKysely();
73
+ const statusRows = await db.selectFrom("message_channel_links").select(["delivery_status", sql`count(*)`.as("count")]).where("external_conversation_id", "in", conversationIds).where("tenant_id", "=", auth.tenantId).where("created_at", ">=", since).groupBy("delivery_status").execute();
74
+ for (const row of statusRows) {
75
+ const value = Number(row.count) || 0;
76
+ totalsLast24h += value;
77
+ const key = row.delivery_status;
78
+ if (key && key in counts) counts[key] += value;
79
+ else counts.other += value;
80
+ }
81
81
  recentFailures = await findWithDecryption(
82
82
  em,
83
83
  MessageChannelLink,
@@ -90,19 +90,13 @@ async function GET(req, context) {
90
90
  dscope
91
91
  );
92
92
  }
93
- const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 };
94
- for (const link of links) {
95
- const key = link.deliveryStatus;
96
- if (key in counts) counts[key] += 1;
97
- else counts.other += 1;
98
- }
99
93
  return NextResponse.json({
100
94
  channelId: channel.id,
101
95
  providerKey: channel.providerKey,
102
96
  channelType: channel.channelType,
103
97
  windowHours: WINDOW_HOURS,
104
98
  counts,
105
- totalsLast24h: links.length,
99
+ totalsLast24h,
106
100
  recentFailures: recentFailures.map((link) => ({
107
101
  id: link.id,
108
102
  messageId: link.messageId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/communication_channels/api/get/channels/%5Bid%5D/health/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel, ExternalConversation, MessageChannelLink } from '../../../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanAccessChannel } from '../../../../../lib/access-control'\n\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/channels/[id]/health',\n GET: { requireAuth: true, requireFeatures: ['communication_channels.view'] },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst WINDOW_HOURS = 24\nconst RECENT_FAILURES_LIMIT = 10\n\n/**\n * Channel health snapshot \u2014 live aggregates from `message_channel_links` rather\n * than a dedicated `HealthLog` table.\n *\n * Returns counts of `sent` / `failed` / `pending` deliveries in the trailing\n * 24-hour window, plus the most recent failed links with last-error context.\n *\n * Per-user isolation: aggregates are scoped to **this channel only** via the\n * `external_conversations.channelId` join (a MessageChannelLink does not have a\n * direct channelId column \u2014 the link goes through ExternalConversation). Before\n * any DB read for aggregates, the channel must pass `assertCanAccessChannel`\n * so a non-admin caller cannot inspect another user's channel telemetry.\n */\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid channel id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const dscope = {\n tenantId: auth.tenantId as string,\n organizationId: (auth as { orgId?: string | null }).orgId ?? null,\n }\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id, tenantId: auth.tenantId, organizationId: dscope.organizationId, deletedAt: null },\n undefined,\n dscope,\n )\n if (!channel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(auth.sub as string, {\n tenantId: auth.tenantId as string,\n organizationId: dscope.organizationId,\n })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanAccessChannel(channel, auth.sub as string, userFeatures)\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n throw err\n }\n\n // ExternalConversation is append-only on the hub side \u2014 no soft-delete column.\n // Scope by channel + tenant so aggregates stay isolated even when the same\n // provider is connected by multiple users in the same tenant.\n const conversations = await findWithDecryption(\n em,\n ExternalConversation,\n { channelId: channel.id, tenantId: auth.tenantId },\n undefined,\n dscope,\n )\n const conversationIds = conversations.map((c) => c.id).filter(Boolean) as string[]\n\n let links: MessageChannelLink[] = []\n let recentFailures: MessageChannelLink[] = []\n if (conversationIds.length > 0) {\n const since = new Date(Date.now() - WINDOW_HOURS * 60 * 60 * 1000)\n links = await findWithDecryption(\n em,\n MessageChannelLink,\n {\n externalConversationId: { $in: conversationIds },\n tenantId: auth.tenantId,\n createdAt: { $gte: since },\n },\n undefined,\n dscope,\n )\n recentFailures = await findWithDecryption(\n em,\n MessageChannelLink,\n {\n externalConversationId: { $in: conversationIds },\n tenantId: auth.tenantId,\n deliveryStatus: 'failed',\n },\n { limit: RECENT_FAILURES_LIMIT, orderBy: { createdAt: 'desc' } },\n dscope,\n )\n }\n\n const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 }\n for (const link of links) {\n const key = link.deliveryStatus as keyof typeof counts\n if (key in counts) counts[key] += 1\n else counts.other += 1\n }\n\n return NextResponse.json({\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n windowHours: WINDOW_HOURS,\n counts,\n totalsLast24h: links.length,\n recentFailures: recentFailures.map((link) => ({\n id: link.id,\n messageId: link.messageId,\n direction: link.direction,\n createdAt: link.createdAt?.toISOString?.() ?? null,\n lastError:\n (link.channelMetadata as Record<string, unknown> | null)?.lastError ?? null,\n transient:\n (link.channelMetadata as Record<string, unknown> | null)?.transient ?? null,\n })),\n })\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'Snapshot of channel delivery health (last 24h)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 200, description: 'Health snapshot' },\n { status: 400, description: 'Invalid channel id' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Channel not found' },\n ],\n },\n },\n}\nexport default GET\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel, ExternalConversation, MessageChannelLink } from '../../../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanAccessChannel } from '../../../../../lib/access-control'\n\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/channels/[id]/health',\n GET: { requireAuth: true, requireFeatures: ['communication_channels.view'] },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst WINDOW_HOURS = 24\nconst RECENT_FAILURES_LIMIT = 10\n\n/**\n * Channel health snapshot \u2014 live aggregates from `message_channel_links` rather\n * than a dedicated `HealthLog` table.\n *\n * Returns counts of `sent` / `failed` / `pending` deliveries in the trailing\n * 24-hour window, plus the most recent failed links with last-error context.\n *\n * Per-user isolation: aggregates are scoped to **this channel only** via the\n * `external_conversations.channelId` join (a MessageChannelLink does not have a\n * direct channelId column \u2014 the link goes through ExternalConversation). Before\n * any DB read for aggregates, the channel must pass `assertCanAccessChannel`\n * so a non-admin caller cannot inspect another user's channel telemetry.\n */\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid channel id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const dscope = {\n tenantId: auth.tenantId as string,\n organizationId: (auth as { orgId?: string | null }).orgId ?? null,\n }\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id, tenantId: auth.tenantId, organizationId: dscope.organizationId, deletedAt: null },\n undefined,\n dscope,\n )\n if (!channel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(auth.sub as string, {\n tenantId: auth.tenantId as string,\n organizationId: dscope.organizationId,\n })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanAccessChannel(channel, auth.sub as string, userFeatures)\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n throw err\n }\n\n // ExternalConversation is append-only on the hub side \u2014 no soft-delete column.\n // Scope by channel + tenant so aggregates stay isolated even when the same\n // provider is connected by multiple users in the same tenant.\n const conversations = await findWithDecryption(\n em,\n ExternalConversation,\n { channelId: channel.id, tenantId: auth.tenantId },\n undefined,\n dscope,\n )\n const conversationIds = conversations.map((c) => c.id).filter(Boolean) as string[]\n\n const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 }\n let totalsLast24h = 0\n let recentFailures: MessageChannelLink[] = []\n if (conversationIds.length > 0) {\n const since = new Date(Date.now() - WINDOW_HOURS * 60 * 60 * 1000)\n\n // Aggregate the delivery-status counts in the database so the response stays\n // a small fixed shape regardless of how much traffic the channel saw in the\n // window. `delivery_status`, `external_conversation_id`, `tenant_id` and\n // `created_at` are plaintext columns (see encryption.ts \u2014 only\n // `channel_ingest_dead_letter.raw_body` is encrypted on this module), so a\n // grouped count is safe without the decryption helpers.\n const db = em.getKysely<any>() as any\n const statusRows = (await db\n .selectFrom('message_channel_links')\n .select(['delivery_status', sql<string>`count(*)`.as('count')])\n .where('external_conversation_id', 'in', conversationIds)\n .where('tenant_id', '=', auth.tenantId)\n .where('created_at', '>=', since)\n .groupBy('delivery_status')\n .execute()) as Array<{ delivery_status: string | null; count: string | number }>\n\n for (const row of statusRows) {\n const value = Number(row.count) || 0\n totalsLast24h += value\n const key = row.delivery_status as keyof typeof counts\n if (key && key in counts) counts[key] += value\n else counts.other += value\n }\n\n recentFailures = await findWithDecryption(\n em,\n MessageChannelLink,\n {\n externalConversationId: { $in: conversationIds },\n tenantId: auth.tenantId,\n deliveryStatus: 'failed',\n },\n { limit: RECENT_FAILURES_LIMIT, orderBy: { createdAt: 'desc' } },\n dscope,\n )\n }\n\n return NextResponse.json({\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n windowHours: WINDOW_HOURS,\n counts,\n totalsLast24h,\n recentFailures: recentFailures.map((link) => ({\n id: link.id,\n messageId: link.messageId,\n direction: link.direction,\n createdAt: link.createdAt?.toISOString?.() ?? null,\n lastError:\n (link.channelMetadata as Record<string, unknown> | null)?.lastError ?? null,\n transient:\n (link.channelMetadata as Record<string, unknown> | null)?.transient ?? null,\n })),\n })\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: 'Snapshot of channel delivery health (last 24h)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 200, description: 'Health snapshot' },\n { status: 400, description: 'Invalid channel id' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Channel not found' },\n ],\n },\n },\n}\nexport default GET\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,8 @@ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuarde
8
8
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
9
9
  import { useT } from "@open-mercato/shared/lib/i18n/context";
10
10
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
11
+ import { hasFeature } from "@open-mercato/shared/security/features";
12
+ const MANAGE_FEATURE = "configs.cache.manage";
11
13
  const API_PATH = "/api/configs/cache";
12
14
  const CACHE_MUTATION_CONTEXT_ID = "configs-cache-panel";
13
15
  function CachePanel() {
@@ -55,7 +57,7 @@ function CachePanel() {
55
57
  {
56
58
  method: "POST",
57
59
  headers: { "content-type": "application/json" },
58
- body: JSON.stringify({ features: ["configs.cache.manage"] })
60
+ body: JSON.stringify({ features: [MANAGE_FEATURE] })
59
61
  },
60
62
  {
61
63
  errorMessage: t("configs.cache.loadError", "Failed to load cache statistics."),
@@ -64,8 +66,8 @@ function CachePanel() {
64
66
  );
65
67
  if (cancelled) return;
66
68
  const granted = Array.isArray(payload?.granted) ? payload.granted.filter((feature) => typeof feature === "string") : [];
67
- const hasFeature = payload?.ok === true || granted.includes("configs.cache.manage");
68
- setCanManage(hasFeature);
69
+ const canManageFeature = payload?.ok === true || hasFeature(granted, MANAGE_FEATURE);
70
+ setCanManage(canManageFeature);
69
71
  } catch {
70
72
  if (!cancelled) setCanManage(false);
71
73
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/configs/components/CachePanel.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\n\nconst API_PATH = '/api/configs/cache'\nconst CACHE_MUTATION_CONTEXT_ID = 'configs-cache-panel'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: CACHE_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const cacheMutationContext = React.useMemo(\n () => ({\n formId: CACHE_MUTATION_CONTEXT_ID,\n resourceKind: 'configs.cache',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['configs.cache.manage'] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')\n setCanManage(hasFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setPurgingAll(true)\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeAll' },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, confirm, purgingAll, t, handleRefresh, runMutation, cacheMutationContext]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeSegment', segment },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, confirm, segmentPurges, t, handleRefresh, runMutation, cacheMutationContext]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" type=\"button\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached responses for this tenant.')}\n </p>\n )}\n </div>\n {ConfirmDialogElement}\n </section>\n )\n}\n\nexport default CachePanel\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\nconst MANAGE_FEATURE = 'configs.cache.manage'\n\nconst API_PATH = '/api/configs/cache'\nconst CACHE_MUTATION_CONTEXT_ID = 'configs-cache-panel'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: CACHE_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const cacheMutationContext = React.useMemo(\n () => ({\n formId: CACHE_MUTATION_CONTEXT_ID,\n resourceKind: 'configs.cache',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: [MANAGE_FEATURE] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const canManageFeature = payload?.ok === true || hasFeature(granted, MANAGE_FEATURE)\n setCanManage(canManageFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setPurgingAll(true)\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeAll' },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, confirm, purgingAll, t, handleRefresh, runMutation, cacheMutationContext]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n const confirmed = await confirm({\n title: t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await runMutation({\n operation: () =>\n readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n ),\n context: cacheMutationContext,\n mutationPayload: { action: 'purgeSegment', segment },\n })\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, confirm, segmentPurges, t, handleRefresh, runMutation, cacheMutationContext]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n {ConfirmDialogElement}\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" type=\"button\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" type=\"button\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached responses for this tenant.')}\n </p>\n )}\n </div>\n {ConfirmDialogElement}\n </section>\n )\n}\n\nexport default CachePanel\n"],
5
+ "mappings": "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",
6
6
  "names": ["stats"]
7
7
  }
@@ -6,7 +6,8 @@ import {
6
6
  changeOverrideStateBaseSchema,
7
7
  overrideListQuerySchema,
8
8
  getToggleOverrideQuerySchema,
9
- featureToggleOverrideResponseSchema
9
+ featureToggleOverrideResponseSchema,
10
+ overrideRowIdSchema
10
11
  } from "../data/validators.js";
11
12
  const featureTogglesTag = "Feature Toggles";
12
13
  const featureToggleErrorSchema = z.object({
@@ -32,14 +33,14 @@ const featureToggleListResponseSchema = z.object({
32
33
  totalPages: z.number().int().min(1)
33
34
  });
34
35
  const featureToggleOverrideSchema = z.object({
35
- id: z.string().uuid(),
36
+ id: overrideRowIdSchema,
36
37
  toggleId: z.string().uuid(),
37
- overrideState: z.enum(["enabled", "disabled", "inherit"]),
38
+ tenantName: z.string(),
39
+ tenantId: z.string().uuid(),
38
40
  identifier: z.string(),
39
41
  name: z.string(),
40
- category: z.string().nullable().optional(),
41
- defaultState: z.boolean(),
42
- tenantName: z.string().nullable().optional()
42
+ category: z.string(),
43
+ isOverride: z.boolean()
43
44
  }).passthrough();
44
45
  const featureToggleOverrideListResponseSchema = z.object({
45
46
  items: z.array(featureToggleOverrideSchema),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/feature_toggles/api/openapi.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport {\n toggleCreateSchema,\n toggleUpdateSchema,\n toggleTypeSchema,\n changeOverrideStateBaseSchema,\n overrideListQuerySchema,\n getToggleOverrideQuerySchema,\n featureToggleOverrideResponseSchema,\n} from '../data/validators'\n\nexport const featureTogglesTag = 'Feature Toggles'\n\nexport const featureToggleErrorSchema = z\n .object({\n error: z.string(),\n details: z.any().optional(),\n })\n .passthrough()\n\nexport const featureToggleSchema = z\n .object({\n id: z.string().uuid(),\n identifier: z.string(),\n name: z.string(),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n type: toggleTypeSchema,\n defaultValue: z.any().nullable().optional(),\n created_at: z.string().optional(),\n updated_at: z.string().optional(),\n })\n .passthrough()\n\nexport const featureToggleListResponseSchema = z.object({\n items: z.array(featureToggleSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n})\n\nexport { toggleCreateSchema, toggleUpdateSchema }\n\nexport const featureToggleOverrideSchema = z\n .object({\n id: z.string().uuid(),\n toggleId: z.string().uuid(),\n overrideState: z.enum(['enabled', 'disabled', 'inherit']),\n identifier: z.string(),\n name: z.string(),\n category: z.string().nullable().optional(),\n defaultState: z.boolean(),\n tenantName: z.string().nullable().optional(),\n })\n .passthrough()\n\nexport const featureToggleOverrideListResponseSchema = z.object({\n items: z.array(featureToggleOverrideSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n isSuperAdmin: z.boolean(),\n})\n\n\nexport { changeOverrideStateBaseSchema, overrideListQuerySchema, getToggleOverrideQuerySchema, featureToggleOverrideResponseSchema }\n\nexport const changeOverrideStateResponseSchema = z.object({\n ok: z.literal(true),\n overrideToggleId: z.string().uuid().nullable(),\n})\n\nexport const checkResponseSchema = z.object({\n enabled: z.boolean(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkStringResponseSchema = z.object({\n valueType: z.literal('string'),\n value: z.string(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkNumberResponseSchema = z.object({\n valueType: z.literal('number'),\n value: z.number(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkJsonResponseSchema = z.object({\n valueType: z.literal('json'),\n value: z.unknown(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAEA,MAAM,oBAAoB;AAE1B,MAAM,2BAA2B,EACnC,OAAO;AAAA,EACJ,OAAO,EAAE,OAAO;AAAA,EAChB,SAAS,EAAE,IAAI,EAAE,SAAS;AAC9B,CAAC,EACA,YAAY;AAEV,MAAM,sBAAsB,EAC9B,OAAO;AAAA,EACJ,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,YAAY,EAAE,OAAO;AAAA,EACrB,MAAM,EAAE,OAAO;AAAA,EACf,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EAC5C,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,MAAM;AAAA,EACN,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EAC1C,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,SAAS;AACpC,CAAC,EACA,YAAY;AAEV,MAAM,kCAAkC,EAAE,OAAO;AAAA,EACpD,OAAO,EAAE,MAAM,mBAAmB;AAAA,EAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AACtC,CAAC;AAIM,MAAM,8BAA8B,EACtC,OAAO;AAAA,EACJ,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,eAAe,EAAE,KAAK,CAAC,WAAW,YAAY,SAAS,CAAC;AAAA,EACxD,YAAY,EAAE,OAAO;AAAA,EACrB,MAAM,EAAE,OAAO;AAAA,EACf,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,cAAc,EAAE,QAAQ;AAAA,EACxB,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAC/C,CAAC,EACA,YAAY;AAEV,MAAM,0CAA0C,EAAE,OAAO;AAAA,EAC5D,OAAO,EAAE,MAAM,2BAA2B;AAAA,EAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAC5B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EAClC,cAAc,EAAE,QAAQ;AAC5B,CAAC;AAKM,MAAM,oCAAoC,EAAE,OAAO;AAAA,EACtD,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,kBAAkB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACjD,CAAC;AAEM,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACxC,SAAS,EAAE,QAAQ;AAAA,EACnB,QAAQ,EAAE,KAAK,CAAC,YAAY,WAAW,YAAY,SAAS,CAAC;AAAA,EAC7D,UAAU,EAAE,OAAO;AAAA,EACnB,YAAY,EAAE,OAAO;AAAA,EACrB,UAAU,EAAE,OAAO;AACvB,CAAC,EAAE,YAAY;AAER,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAC9C,WAAW,EAAE,QAAQ,QAAQ;AAAA,EAC7B,OAAO,EAAE,OAAO;AAAA,EAChB,QAAQ,EAAE,KAAK,CAAC,YAAY,WAAW,YAAY,SAAS,CAAC;AAAA,EAC7D,UAAU,EAAE,OAAO;AAAA,EACnB,YAAY,EAAE,OAAO;AAAA,EACrB,UAAU,EAAE,OAAO;AACvB,CAAC,EAAE,YAAY;AAER,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAC9C,WAAW,EAAE,QAAQ,QAAQ;AAAA,EAC7B,OAAO,EAAE,OAAO;AAAA,EAChB,QAAQ,EAAE,KAAK,CAAC,YAAY,WAAW,YAAY,SAAS,CAAC;AAAA,EAC7D,UAAU,EAAE,OAAO;AAAA,EACnB,YAAY,EAAE,OAAO;AAAA,EACrB,UAAU,EAAE,OAAO;AACvB,CAAC,EAAE,YAAY;AAER,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC5C,WAAW,EAAE,QAAQ,MAAM;AAAA,EAC3B,OAAO,EAAE,QAAQ;AAAA,EACjB,QAAQ,EAAE,KAAK,CAAC,YAAY,WAAW,YAAY,SAAS,CAAC;AAAA,EAC7D,UAAU,EAAE,OAAO;AAAA,EACnB,YAAY,EAAE,OAAO;AAAA,EACrB,UAAU,EAAE,OAAO;AACvB,CAAC,EAAE,YAAY;",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport {\n toggleCreateSchema,\n toggleUpdateSchema,\n toggleTypeSchema,\n changeOverrideStateBaseSchema,\n overrideListQuerySchema,\n getToggleOverrideQuerySchema,\n featureToggleOverrideResponseSchema,\n overrideRowIdSchema,\n} from '../data/validators'\n\nexport const featureTogglesTag = 'Feature Toggles'\n\nexport const featureToggleErrorSchema = z\n .object({\n error: z.string(),\n details: z.any().optional(),\n })\n .passthrough()\n\nexport const featureToggleSchema = z\n .object({\n id: z.string().uuid(),\n identifier: z.string(),\n name: z.string(),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n type: toggleTypeSchema,\n defaultValue: z.any().nullable().optional(),\n created_at: z.string().optional(),\n updated_at: z.string().optional(),\n })\n .passthrough()\n\nexport const featureToggleListResponseSchema = z.object({\n items: z.array(featureToggleSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n})\n\nexport { toggleCreateSchema, toggleUpdateSchema }\n\n// Documents the actual override-list row shape returned by GET /api/feature_toggles/overrides\n// (see lib/queries.ts \u2192 getOverrides). Each row reports whether the tenant has a\n// per-tenant override via `isOverride`; inherited rows (no override) carry an\n// empty-string `id` sentinel instead of a UUID, hence `overrideRowIdSchema`.\nexport const featureToggleOverrideSchema = z\n .object({\n id: overrideRowIdSchema,\n toggleId: z.string().uuid(),\n tenantName: z.string(),\n tenantId: z.string().uuid(),\n identifier: z.string(),\n name: z.string(),\n category: z.string(),\n isOverride: z.boolean(),\n })\n .passthrough()\n\nexport const featureToggleOverrideListResponseSchema = z.object({\n items: z.array(featureToggleOverrideSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n isSuperAdmin: z.boolean(),\n})\n\n\nexport { changeOverrideStateBaseSchema, overrideListQuerySchema, getToggleOverrideQuerySchema, featureToggleOverrideResponseSchema }\n\nexport const changeOverrideStateResponseSchema = z.object({\n ok: z.literal(true),\n overrideToggleId: z.string().uuid().nullable(),\n})\n\nexport const checkResponseSchema = z.object({\n enabled: z.boolean(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkStringResponseSchema = z.object({\n valueType: z.literal('string'),\n value: z.string(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkNumberResponseSchema = z.object({\n valueType: z.literal('number'),\n value: z.number(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n\nexport const checkJsonResponseSchema = z.object({\n valueType: z.literal('json'),\n value: z.unknown(),\n source: z.enum(['override', 'default', 'fallback', 'missing']),\n toggleId: z.string(),\n identifier: z.string(),\n tenantId: z.string(),\n}).passthrough()\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -61,8 +61,9 @@ const featureToggleSchema = z.object({
61
61
  createdAt: z.string().optional(),
62
62
  updatedAt: z.string().optional()
63
63
  });
64
+ const overrideRowIdSchema = z.union([z.string().uuid(), z.literal("")]);
64
65
  const overrideListResponseSchema = z.object({
65
- id: z.string().uuid(),
66
+ id: overrideRowIdSchema,
66
67
  toggleId: z.string().uuid(),
67
68
  tenantName: z.string(),
68
69
  tenantId: z.string().uuid(),
@@ -81,6 +82,7 @@ export {
81
82
  getToggleOverrideQuerySchema,
82
83
  overrideListQuerySchema,
83
84
  overrideListResponseSchema,
85
+ overrideRowIdSchema,
84
86
  processedChangeOverrideStateSchema,
85
87
  toggleCreateSchema,
86
88
  toggleCreateSchemaList,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/feature_toggles/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\n\n// Namespaced feature-flag identifiers use dots and dashes as separators\n// (e.g. the seeded `customers.interactions.legacy-adapters`), so the validator\n// accepts lowercase letters, digits, `_`, `.`, and `-` after the leading\n// lowercase letter (#2055 QA round-6).\nexport const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_.-]*$/\n\nexport const IDENTIFIER_PATTERN_MESSAGE =\n 'identifier must start with a lowercase letter and contain only lowercase letters, numbers, dots, dashes, and underscores'\n\nexport const toggleTypeSchema = z.enum(['boolean', 'string', 'number', 'json'])\n\nexport const toggleCreateSchema = z.object({\n identifier: z.string().min(1).regex(\n IDENTIFIER_PATTERN,\n IDENTIFIER_PATTERN_MESSAGE,\n ),\n name: z.string().min(1),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n\n type: toggleTypeSchema,\n defaultValue: z.any().nullable().optional(),\n})\n\nexport const toggleCreateSchemaList = z.array(toggleCreateSchema)\n\nexport const toggleUpdateSchema = z\n .object({\n id: z.string().uuid(),\n })\n .merge(toggleCreateSchema.partial())\n\nexport const changeOverrideStateBaseSchema = z.object({\n toggleId: z.string().uuid(),\n isOverride: z.boolean(),\n overrideValue: z.any().optional(),\n})\n\n\nexport const processedChangeOverrideStateSchema = changeOverrideStateBaseSchema.extend({\n tenantId: z.string().uuid(),\n})\n\nexport const featureToggleOverrideResponseSchema = z.object({\n id: z.string().uuid(),\n value: z.any().optional(),\n tenantName: z.string(),\n tenantId: z.string().uuid(),\n toggleType: toggleTypeSchema,\n updatedAt: z.string().nullable().optional(),\n})\n\nexport type FeatureToggleOverrideResponse = z.infer<typeof featureToggleOverrideResponseSchema>\n\nexport const featureToggleOverrideWithValueSchema = featureToggleOverrideResponseSchema\nexport type FeatureToggleOverrideWithValue = FeatureToggleOverrideResponse\n\nexport const getToggleOverrideQuerySchema = z.object({\n toggleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const overrideListQuerySchema = z\n .object({\n category: z.string().optional(),\n name: z.string().optional(),\n identifier: z.string().optional(),\n sortField: z.enum(['identifier', 'name', 'category']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n page: z.coerce.number().min(1).optional().default(1),\n pageSize: z.coerce.number().min(1).max(100).optional().default(25),\n })\n .passthrough()\n\nexport const featureToggleSchema = z.object({\n id: z.string().uuid().optional(),\n identifier: z.string().min(1).regex(\n IDENTIFIER_PATTERN,\n IDENTIFIER_PATTERN_MESSAGE,\n ),\n name: z.string().min(1),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n\n type: z.enum(['boolean', 'string', 'number', 'json']),\n defaultValue: z.any().nullable().optional(),\n createdAt: z.string().optional(),\n updatedAt: z.string().optional(),\n})\n\nexport const overrideListResponseSchema = z.object({\n id: z.string().uuid(),\n toggleId: z.string().uuid(),\n tenantName: z.string(),\n tenantId: z.string().uuid(),\n identifier: z.string(),\n name: z.string(),\n category: z.string(),\n isOverride: z.boolean(),\n});\n\nexport type OverrideListResponse = z.infer<typeof overrideListResponseSchema>\n\nexport type ToggleCreateInput = z.infer<typeof toggleCreateSchema>\nexport type ToggleUpdateInput = z.infer<typeof toggleUpdateSchema>\nexport type ChangeOverrideStateBaseInput = z.infer<typeof changeOverrideStateBaseSchema>\nexport type ProcessedChangeOverrideStateInput = z.infer<typeof processedChangeOverrideStateSchema>\nexport type GetToggleOverrideQuery = z.infer<typeof getToggleOverrideQuerySchema>\nexport type GetOverridesQuery = z.infer<typeof overrideListQuerySchema>\nexport type FeatureToggle = z.infer<typeof featureToggleSchema>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\n\n// Namespaced feature-flag identifiers use dots and dashes as separators\n// (e.g. the seeded `customers.interactions.legacy-adapters`), so the validator\n// accepts lowercase letters, digits, `_`, `.`, and `-` after the leading\n// lowercase letter (#2055 QA round-6).\nexport const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_.-]*$/\n\nexport const IDENTIFIER_PATTERN_MESSAGE =\n 'identifier must start with a lowercase letter and contain only lowercase letters, numbers, dots, dashes, and underscores'\n\nexport const toggleTypeSchema = z.enum(['boolean', 'string', 'number', 'json'])\n\nexport const toggleCreateSchema = z.object({\n identifier: z.string().min(1).regex(\n IDENTIFIER_PATTERN,\n IDENTIFIER_PATTERN_MESSAGE,\n ),\n name: z.string().min(1),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n\n type: toggleTypeSchema,\n defaultValue: z.any().nullable().optional(),\n})\n\nexport const toggleCreateSchemaList = z.array(toggleCreateSchema)\n\nexport const toggleUpdateSchema = z\n .object({\n id: z.string().uuid(),\n })\n .merge(toggleCreateSchema.partial())\n\nexport const changeOverrideStateBaseSchema = z.object({\n toggleId: z.string().uuid(),\n isOverride: z.boolean(),\n overrideValue: z.any().optional(),\n})\n\n\nexport const processedChangeOverrideStateSchema = changeOverrideStateBaseSchema.extend({\n tenantId: z.string().uuid(),\n})\n\nexport const featureToggleOverrideResponseSchema = z.object({\n id: z.string().uuid(),\n value: z.any().optional(),\n tenantName: z.string(),\n tenantId: z.string().uuid(),\n toggleType: toggleTypeSchema,\n updatedAt: z.string().nullable().optional(),\n})\n\nexport type FeatureToggleOverrideResponse = z.infer<typeof featureToggleOverrideResponseSchema>\n\nexport const featureToggleOverrideWithValueSchema = featureToggleOverrideResponseSchema\nexport type FeatureToggleOverrideWithValue = FeatureToggleOverrideResponse\n\nexport const getToggleOverrideQuerySchema = z.object({\n toggleId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const overrideListQuerySchema = z\n .object({\n category: z.string().optional(),\n name: z.string().optional(),\n identifier: z.string().optional(),\n sortField: z.enum(['identifier', 'name', 'category']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n page: z.coerce.number().min(1).optional().default(1),\n pageSize: z.coerce.number().min(1).max(100).optional().default(25),\n })\n .passthrough()\n\nexport const featureToggleSchema = z.object({\n id: z.string().uuid().optional(),\n identifier: z.string().min(1).regex(\n IDENTIFIER_PATTERN,\n IDENTIFIER_PATTERN_MESSAGE,\n ),\n name: z.string().min(1),\n description: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n\n type: z.enum(['boolean', 'string', 'number', 'json']),\n defaultValue: z.any().nullable().optional(),\n createdAt: z.string().optional(),\n updatedAt: z.string().optional(),\n})\n\n// Inherited rows (no per-tenant override) intentionally carry an empty-string\n// `id` sentinel instead of a UUID, so the row id must accept both a UUID (real\n// override rows) and the empty inherited sentinel. See lib/queries.ts.\nexport const overrideRowIdSchema = z.union([z.string().uuid(), z.literal('')])\n\nexport const overrideListResponseSchema = z.object({\n id: overrideRowIdSchema,\n toggleId: z.string().uuid(),\n tenantName: z.string(),\n tenantId: z.string().uuid(),\n identifier: z.string(),\n name: z.string(),\n category: z.string(),\n isOverride: z.boolean(),\n});\n\nexport type OverrideListResponse = z.infer<typeof overrideListResponseSchema>\n\nexport type ToggleCreateInput = z.infer<typeof toggleCreateSchema>\nexport type ToggleUpdateInput = z.infer<typeof toggleUpdateSchema>\nexport type ChangeOverrideStateBaseInput = z.infer<typeof changeOverrideStateBaseSchema>\nexport type ProcessedChangeOverrideStateInput = z.infer<typeof processedChangeOverrideStateSchema>\nexport type GetToggleOverrideQuery = z.infer<typeof getToggleOverrideQuerySchema>\nexport type GetOverridesQuery = z.infer<typeof overrideListQuerySchema>\nexport type FeatureToggle = z.infer<typeof featureToggleSchema>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,10 +4,10 @@ import { getIntegrationTitle } from "@open-mercato/shared/modules/integrations/t
4
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
5
5
  import { cn } from "@open-mercato/shared/lib/utils";
6
6
  const SYNC_STATUS_STYLES = {
7
- synced: { dot: "bg-green-500", label: "integrations.syncStatus.synced" },
8
- pending: { dot: "bg-yellow-500", label: "integrations.syncStatus.pending" },
9
- error: { dot: "bg-red-500", label: "integrations.syncStatus.error" },
10
- not_synced: { dot: "bg-gray-400", label: "integrations.syncStatus.notSynced" }
7
+ synced: { dot: "bg-status-success-icon", label: "integrations.syncStatus.synced" },
8
+ pending: { dot: "bg-status-warning-icon", label: "integrations.syncStatus.pending" },
9
+ error: { dot: "bg-status-error-icon", label: "integrations.syncStatus.error" },
10
+ not_synced: { dot: "bg-status-neutral-icon", label: "integrations.syncStatus.notSynced" }
11
11
  };
12
12
  function SyncStatusBadge({ status, lastSynced }) {
13
13
  const t = useT();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/integrations/widgets/injection/external-ids/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport type { ExternalIdMapping } from '@open-mercato/shared/modules/integrations/types'\nimport { getIntegrationTitle } from '@open-mercato/shared/modules/integrations/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\ntype IntegrationsData = Record<string, ExternalIdMapping>\n\nconst SYNC_STATUS_STYLES: Record<ExternalIdMapping['syncStatus'], { dot: string; label: string }> = {\n synced: { dot: 'bg-green-500', label: 'integrations.syncStatus.synced' },\n pending: { dot: 'bg-yellow-500', label: 'integrations.syncStatus.pending' },\n error: { dot: 'bg-red-500', label: 'integrations.syncStatus.error' },\n not_synced: { dot: 'bg-gray-400', label: 'integrations.syncStatus.notSynced' },\n}\n\nfunction SyncStatusBadge({ status, lastSynced }: { status: ExternalIdMapping['syncStatus']; lastSynced?: string }) {\n const t = useT()\n const config = SYNC_STATUS_STYLES[status]\n const label = t(config.label, status)\n\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <span className={cn('inline-block size-1.5 rounded-full', config.dot)} aria-hidden=\"true\" />\n <span>{label}</span>\n {lastSynced && (\n <span title={lastSynced}>\n {new Date(lastSynced).toLocaleDateString()}\n </span>\n )}\n </span>\n )\n}\n\nfunction ExternalLinkIcon() {\n return (\n <svg className=\"size-3.5\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path d=\"M6.5 3.5H3a1 1 0 0 0-1 1V13a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1V9.5\" />\n <path d=\"M9.5 2h4.5v4.5M14 2 7.5 8.5\" />\n </svg>\n )\n}\n\nexport default function ExternalIdsWidget({ data }: InjectionWidgetComponentProps) {\n const t = useT()\n const integrations = (data as Record<string, unknown>)?._integrations as IntegrationsData | undefined\n\n if (!integrations || Object.keys(integrations).length === 0) return null\n\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <h3 className=\"mb-3 text-sm font-medium\">\n {t('integrations.externalIds.title', 'External IDs')}\n </h3>\n <div className=\"space-y-2\">\n {Object.entries(integrations).map(([integrationId, mapping]) => (\n <div\n key={integrationId}\n className=\"flex items-center justify-between gap-2 rounded-md bg-muted/50 px-3 py-2\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <span className=\"text-sm font-medium truncate\">\n {getIntegrationTitle(integrationId)}\n </span>\n <code className=\"rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground\">\n {mapping.externalId}\n </code>\n {mapping.externalUrl && (\n <a\n href={mapping.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-foreground transition-colors\"\n aria-label={t('integrations.externalIds.openExternal', 'Open in external system')}\n >\n <ExternalLinkIcon />\n </a>\n )}\n </div>\n <SyncStatusBadge status={mapping.syncStatus} lastSynced={mapping.lastSyncedAt} />\n </div>\n ))}\n </div>\n </div>\n )\n}\n\nExternalIdsWidget.metadata = {\n id: 'integrations.injection.external-ids',\n title: 'External IDs',\n features: ['integrations.view'],\n}\n"],
5
- "mappings": ";AAwBI,SACE,KADF;AAnBJ,SAAS,2BAA2B;AACpC,SAAS,YAAY;AACrB,SAAS,UAAU;AAInB,MAAM,qBAA8F;AAAA,EAClG,QAAQ,EAAE,KAAK,gBAAgB,OAAO,iCAAiC;AAAA,EACvE,SAAS,EAAE,KAAK,iBAAiB,OAAO,kCAAkC;AAAA,EAC1E,OAAO,EAAE,KAAK,cAAc,OAAO,gCAAgC;AAAA,EACnE,YAAY,EAAE,KAAK,eAAe,OAAO,oCAAoC;AAC/E;AAEA,SAAS,gBAAgB,EAAE,QAAQ,WAAW,GAAqE;AACjH,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,mBAAmB,MAAM;AACxC,QAAM,QAAQ,EAAE,OAAO,OAAO,MAAM;AAEpC,SACE,qBAAC,UAAK,WAAU,gEACd;AAAA,wBAAC,UAAK,WAAW,GAAG,sCAAsC,OAAO,GAAG,GAAG,eAAY,QAAO;AAAA,IAC1F,oBAAC,UAAM,iBAAM;AAAA,IACZ,cACC,oBAAC,UAAK,OAAO,YACV,cAAI,KAAK,UAAU,EAAE,mBAAmB,GAC3C;AAAA,KAEJ;AAEJ;AAEA,SAAS,mBAAmB;AAC1B,SACE,qBAAC,SAAI,WAAU,YAAW,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAC1F;AAAA,wBAAC,UAAK,GAAE,mEAAkE;AAAA,IAC1E,oBAAC,UAAK,GAAE,+BAA8B;AAAA,KACxC;AAEJ;AAEe,SAAR,kBAAmC,EAAE,KAAK,GAAkC;AACjF,QAAM,IAAI,KAAK;AACf,QAAM,eAAgB,MAAkC;AAExD,MAAI,CAAC,gBAAgB,OAAO,KAAK,YAAY,EAAE,WAAW,EAAG,QAAO;AAEpE,SACE,qBAAC,SAAI,WAAU,iCACb;AAAA,wBAAC,QAAG,WAAU,4BACX,YAAE,kCAAkC,cAAc,GACrD;AAAA,IACA,oBAAC,SAAI,WAAU,aACZ,iBAAO,QAAQ,YAAY,EAAE,IAAI,CAAC,CAAC,eAAe,OAAO,MACxD;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAEV;AAAA,+BAAC,SAAI,WAAU,mCACb;AAAA,gCAAC,UAAK,WAAU,gCACb,8BAAoB,aAAa,GACpC;AAAA,YACA,oBAAC,UAAK,WAAU,0EACb,kBAAQ,YACX;AAAA,YACC,QAAQ,eACP;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,QAAQ;AAAA,gBACd,QAAO;AAAA,gBACP,KAAI;AAAA,gBACJ,WAAU;AAAA,gBACV,cAAY,EAAE,yCAAyC,yBAAyB;AAAA,gBAEhF,8BAAC,oBAAiB;AAAA;AAAA,YACpB;AAAA,aAEJ;AAAA,UACA,oBAAC,mBAAgB,QAAQ,QAAQ,YAAY,YAAY,QAAQ,cAAc;AAAA;AAAA;AAAA,MAtB1E;AAAA,IAuBP,CACD,GACH;AAAA,KACF;AAEJ;AAEA,kBAAkB,WAAW;AAAA,EAC3B,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,UAAU,CAAC,mBAAmB;AAChC;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport type { ExternalIdMapping } from '@open-mercato/shared/modules/integrations/types'\nimport { getIntegrationTitle } from '@open-mercato/shared/modules/integrations/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\ntype IntegrationsData = Record<string, ExternalIdMapping>\n\nconst SYNC_STATUS_STYLES: Record<ExternalIdMapping['syncStatus'], { dot: string; label: string }> = {\n synced: { dot: 'bg-status-success-icon', label: 'integrations.syncStatus.synced' },\n pending: { dot: 'bg-status-warning-icon', label: 'integrations.syncStatus.pending' },\n error: { dot: 'bg-status-error-icon', label: 'integrations.syncStatus.error' },\n not_synced: { dot: 'bg-status-neutral-icon', label: 'integrations.syncStatus.notSynced' },\n}\n\nfunction SyncStatusBadge({ status, lastSynced }: { status: ExternalIdMapping['syncStatus']; lastSynced?: string }) {\n const t = useT()\n const config = SYNC_STATUS_STYLES[status]\n const label = t(config.label, status)\n\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <span className={cn('inline-block size-1.5 rounded-full', config.dot)} aria-hidden=\"true\" />\n <span>{label}</span>\n {lastSynced && (\n <span title={lastSynced}>\n {new Date(lastSynced).toLocaleDateString()}\n </span>\n )}\n </span>\n )\n}\n\nfunction ExternalLinkIcon() {\n return (\n <svg className=\"size-3.5\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path d=\"M6.5 3.5H3a1 1 0 0 0-1 1V13a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1V9.5\" />\n <path d=\"M9.5 2h4.5v4.5M14 2 7.5 8.5\" />\n </svg>\n )\n}\n\nexport default function ExternalIdsWidget({ data }: InjectionWidgetComponentProps) {\n const t = useT()\n const integrations = (data as Record<string, unknown>)?._integrations as IntegrationsData | undefined\n\n if (!integrations || Object.keys(integrations).length === 0) return null\n\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <h3 className=\"mb-3 text-sm font-medium\">\n {t('integrations.externalIds.title', 'External IDs')}\n </h3>\n <div className=\"space-y-2\">\n {Object.entries(integrations).map(([integrationId, mapping]) => (\n <div\n key={integrationId}\n className=\"flex items-center justify-between gap-2 rounded-md bg-muted/50 px-3 py-2\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <span className=\"text-sm font-medium truncate\">\n {getIntegrationTitle(integrationId)}\n </span>\n <code className=\"rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground\">\n {mapping.externalId}\n </code>\n {mapping.externalUrl && (\n <a\n href={mapping.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-foreground transition-colors\"\n aria-label={t('integrations.externalIds.openExternal', 'Open in external system')}\n >\n <ExternalLinkIcon />\n </a>\n )}\n </div>\n <SyncStatusBadge status={mapping.syncStatus} lastSynced={mapping.lastSyncedAt} />\n </div>\n ))}\n </div>\n </div>\n )\n}\n\nExternalIdsWidget.metadata = {\n id: 'integrations.injection.external-ids',\n title: 'External IDs',\n features: ['integrations.view'],\n}\n"],
5
+ "mappings": ";AAwBI,SACE,KADF;AAnBJ,SAAS,2BAA2B;AACpC,SAAS,YAAY;AACrB,SAAS,UAAU;AAInB,MAAM,qBAA8F;AAAA,EAClG,QAAQ,EAAE,KAAK,0BAA0B,OAAO,iCAAiC;AAAA,EACjF,SAAS,EAAE,KAAK,0BAA0B,OAAO,kCAAkC;AAAA,EACnF,OAAO,EAAE,KAAK,wBAAwB,OAAO,gCAAgC;AAAA,EAC7E,YAAY,EAAE,KAAK,0BAA0B,OAAO,oCAAoC;AAC1F;AAEA,SAAS,gBAAgB,EAAE,QAAQ,WAAW,GAAqE;AACjH,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,mBAAmB,MAAM;AACxC,QAAM,QAAQ,EAAE,OAAO,OAAO,MAAM;AAEpC,SACE,qBAAC,UAAK,WAAU,gEACd;AAAA,wBAAC,UAAK,WAAW,GAAG,sCAAsC,OAAO,GAAG,GAAG,eAAY,QAAO;AAAA,IAC1F,oBAAC,UAAM,iBAAM;AAAA,IACZ,cACC,oBAAC,UAAK,OAAO,YACV,cAAI,KAAK,UAAU,EAAE,mBAAmB,GAC3C;AAAA,KAEJ;AAEJ;AAEA,SAAS,mBAAmB;AAC1B,SACE,qBAAC,SAAI,WAAU,YAAW,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAC1F;AAAA,wBAAC,UAAK,GAAE,mEAAkE;AAAA,IAC1E,oBAAC,UAAK,GAAE,+BAA8B;AAAA,KACxC;AAEJ;AAEe,SAAR,kBAAmC,EAAE,KAAK,GAAkC;AACjF,QAAM,IAAI,KAAK;AACf,QAAM,eAAgB,MAAkC;AAExD,MAAI,CAAC,gBAAgB,OAAO,KAAK,YAAY,EAAE,WAAW,EAAG,QAAO;AAEpE,SACE,qBAAC,SAAI,WAAU,iCACb;AAAA,wBAAC,QAAG,WAAU,4BACX,YAAE,kCAAkC,cAAc,GACrD;AAAA,IACA,oBAAC,SAAI,WAAU,aACZ,iBAAO,QAAQ,YAAY,EAAE,IAAI,CAAC,CAAC,eAAe,OAAO,MACxD;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAEV;AAAA,+BAAC,SAAI,WAAU,mCACb;AAAA,gCAAC,UAAK,WAAU,gCACb,8BAAoB,aAAa,GACpC;AAAA,YACA,oBAAC,UAAK,WAAU,0EACb,kBAAQ,YACX;AAAA,YACC,QAAQ,eACP;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,QAAQ;AAAA,gBACd,QAAO;AAAA,gBACP,KAAI;AAAA,gBACJ,WAAU;AAAA,gBACV,cAAY,EAAE,yCAAyC,yBAAyB;AAAA,gBAEhF,8BAAC,oBAAiB;AAAA;AAAA,YACpB;AAAA,aAEJ;AAAA,UACA,oBAAC,mBAAgB,QAAQ,QAAQ,YAAY,YAAY,QAAQ,cAAc;AAAA;AAAA;AAAA,MAtB1E;AAAA,IAuBP,CACD,GACH;AAAA,KACF;AAEJ;AAEA,kBAAkB,WAAW;AAAA,EAC3B,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,UAAU,CAAC,mBAAmB;AAChC;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6121.1.5797a901b3",
3
+ "version": "0.6.6-develop.6130.1.7e07d10dbb",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6121.1.5797a901b3",
250
- "@open-mercato/shared": "0.6.6-develop.6121.1.5797a901b3",
251
- "@open-mercato/ui": "0.6.6-develop.6121.1.5797a901b3",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6130.1.7e07d10dbb",
250
+ "@open-mercato/shared": "0.6.6-develop.6130.1.7e07d10dbb",
251
+ "@open-mercato/ui": "0.6.6-develop.6130.1.7e07d10dbb",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6121.1.5797a901b3",
257
- "@open-mercato/shared": "0.6.6-develop.6121.1.5797a901b3",
258
- "@open-mercato/ui": "0.6.6-develop.6121.1.5797a901b3",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6130.1.7e07d10dbb",
257
+ "@open-mercato/shared": "0.6.6-develop.6130.1.7e07d10dbb",
258
+ "@open-mercato/ui": "0.6.6-develop.6130.1.7e07d10dbb",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
+ import { sql } from 'kysely'
3
4
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
6
  import type { EntityManager } from '@mikro-orm/postgresql'
@@ -100,21 +101,36 @@ export async function GET(req: Request, context: RouteContext): Promise<Response
100
101
  )
101
102
  const conversationIds = conversations.map((c) => c.id).filter(Boolean) as string[]
102
103
 
103
- let links: MessageChannelLink[] = []
104
+ const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 }
105
+ let totalsLast24h = 0
104
106
  let recentFailures: MessageChannelLink[] = []
105
107
  if (conversationIds.length > 0) {
106
108
  const since = new Date(Date.now() - WINDOW_HOURS * 60 * 60 * 1000)
107
- links = await findWithDecryption(
108
- em,
109
- MessageChannelLink,
110
- {
111
- externalConversationId: { $in: conversationIds },
112
- tenantId: auth.tenantId,
113
- createdAt: { $gte: since },
114
- },
115
- undefined,
116
- dscope,
117
- )
109
+
110
+ // Aggregate the delivery-status counts in the database so the response stays
111
+ // a small fixed shape regardless of how much traffic the channel saw in the
112
+ // window. `delivery_status`, `external_conversation_id`, `tenant_id` and
113
+ // `created_at` are plaintext columns (see encryption.ts — only
114
+ // `channel_ingest_dead_letter.raw_body` is encrypted on this module), so a
115
+ // grouped count is safe without the decryption helpers.
116
+ const db = em.getKysely<any>() as any
117
+ const statusRows = (await db
118
+ .selectFrom('message_channel_links')
119
+ .select(['delivery_status', sql<string>`count(*)`.as('count')])
120
+ .where('external_conversation_id', 'in', conversationIds)
121
+ .where('tenant_id', '=', auth.tenantId)
122
+ .where('created_at', '>=', since)
123
+ .groupBy('delivery_status')
124
+ .execute()) as Array<{ delivery_status: string | null; count: string | number }>
125
+
126
+ for (const row of statusRows) {
127
+ const value = Number(row.count) || 0
128
+ totalsLast24h += value
129
+ const key = row.delivery_status as keyof typeof counts
130
+ if (key && key in counts) counts[key] += value
131
+ else counts.other += value
132
+ }
133
+
118
134
  recentFailures = await findWithDecryption(
119
135
  em,
120
136
  MessageChannelLink,
@@ -128,20 +144,13 @@ export async function GET(req: Request, context: RouteContext): Promise<Response
128
144
  )
129
145
  }
130
146
 
131
- const counts = { sent: 0, delivered: 0, read: 0, failed: 0, pending: 0, queued: 0, other: 0 }
132
- for (const link of links) {
133
- const key = link.deliveryStatus as keyof typeof counts
134
- if (key in counts) counts[key] += 1
135
- else counts.other += 1
136
- }
137
-
138
147
  return NextResponse.json({
139
148
  channelId: channel.id,
140
149
  providerKey: channel.providerKey,
141
150
  channelType: channel.channelType,
142
151
  windowHours: WINDOW_HOURS,
143
152
  counts,
144
- totalsLast24h: links.length,
153
+ totalsLast24h,
145
154
  recentFailures: recentFailures.map((link) => ({
146
155
  id: link.id,
147
156
  messageId: link.messageId,
@@ -8,6 +8,9 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
8
8
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
10
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
11
+ import { hasFeature } from '@open-mercato/shared/security/features'
12
+
13
+ const MANAGE_FEATURE = 'configs.cache.manage'
11
14
 
12
15
  const API_PATH = '/api/configs/cache'
13
16
  const CACHE_MUTATION_CONTEXT_ID = 'configs-cache-panel'
@@ -87,7 +90,7 @@ export function CachePanel() {
87
90
  {
88
91
  method: 'POST',
89
92
  headers: { 'content-type': 'application/json' },
90
- body: JSON.stringify({ features: ['configs.cache.manage'] }),
93
+ body: JSON.stringify({ features: [MANAGE_FEATURE] }),
91
94
  },
92
95
  {
93
96
  errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),
@@ -98,8 +101,8 @@ export function CachePanel() {
98
101
  const granted = Array.isArray(payload?.granted)
99
102
  ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]
100
103
  : []
101
- const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')
102
- setCanManage(hasFeature)
104
+ const canManageFeature = payload?.ok === true || hasFeature(granted, MANAGE_FEATURE)
105
+ setCanManage(canManageFeature)
103
106
  } catch {
104
107
  if (!cancelled) setCanManage(false)
105
108
  } finally {
@@ -7,6 +7,7 @@ import {
7
7
  overrideListQuerySchema,
8
8
  getToggleOverrideQuerySchema,
9
9
  featureToggleOverrideResponseSchema,
10
+ overrideRowIdSchema,
10
11
  } from '../data/validators'
11
12
 
12
13
  export const featureTogglesTag = 'Feature Toggles'
@@ -42,16 +43,20 @@ export const featureToggleListResponseSchema = z.object({
42
43
 
43
44
  export { toggleCreateSchema, toggleUpdateSchema }
44
45
 
46
+ // Documents the actual override-list row shape returned by GET /api/feature_toggles/overrides
47
+ // (see lib/queries.ts → getOverrides). Each row reports whether the tenant has a
48
+ // per-tenant override via `isOverride`; inherited rows (no override) carry an
49
+ // empty-string `id` sentinel instead of a UUID, hence `overrideRowIdSchema`.
45
50
  export const featureToggleOverrideSchema = z
46
51
  .object({
47
- id: z.string().uuid(),
52
+ id: overrideRowIdSchema,
48
53
  toggleId: z.string().uuid(),
49
- overrideState: z.enum(['enabled', 'disabled', 'inherit']),
54
+ tenantName: z.string(),
55
+ tenantId: z.string().uuid(),
50
56
  identifier: z.string(),
51
57
  name: z.string(),
52
- category: z.string().nullable().optional(),
53
- defaultState: z.boolean(),
54
- tenantName: z.string().nullable().optional(),
58
+ category: z.string(),
59
+ isOverride: z.boolean(),
55
60
  })
56
61
  .passthrough()
57
62
 
@@ -90,8 +90,13 @@ export const featureToggleSchema = z.object({
90
90
  updatedAt: z.string().optional(),
91
91
  })
92
92
 
93
+ // Inherited rows (no per-tenant override) intentionally carry an empty-string
94
+ // `id` sentinel instead of a UUID, so the row id must accept both a UUID (real
95
+ // override rows) and the empty inherited sentinel. See lib/queries.ts.
96
+ export const overrideRowIdSchema = z.union([z.string().uuid(), z.literal('')])
97
+
93
98
  export const overrideListResponseSchema = z.object({
94
- id: z.string().uuid(),
99
+ id: overrideRowIdSchema,
95
100
  toggleId: z.string().uuid(),
96
101
  tenantName: z.string(),
97
102
  tenantId: z.string().uuid(),
@@ -10,10 +10,10 @@ import { cn } from '@open-mercato/shared/lib/utils'
10
10
  type IntegrationsData = Record<string, ExternalIdMapping>
11
11
 
12
12
  const SYNC_STATUS_STYLES: Record<ExternalIdMapping['syncStatus'], { dot: string; label: string }> = {
13
- synced: { dot: 'bg-green-500', label: 'integrations.syncStatus.synced' },
14
- pending: { dot: 'bg-yellow-500', label: 'integrations.syncStatus.pending' },
15
- error: { dot: 'bg-red-500', label: 'integrations.syncStatus.error' },
16
- not_synced: { dot: 'bg-gray-400', label: 'integrations.syncStatus.notSynced' },
13
+ synced: { dot: 'bg-status-success-icon', label: 'integrations.syncStatus.synced' },
14
+ pending: { dot: 'bg-status-warning-icon', label: 'integrations.syncStatus.pending' },
15
+ error: { dot: 'bg-status-error-icon', label: 'integrations.syncStatus.error' },
16
+ not_synced: { dot: 'bg-status-neutral-icon', label: 'integrations.syncStatus.notSynced' },
17
17
  }
18
18
 
19
19
  function SyncStatusBadge({ status, lastSynced }: { status: ExternalIdMapping['syncStatus']; lastSynced?: string }) {