@open-mercato/core 0.8.1-develop.7225.1.73190a4a95 → 0.8.1-develop.7227.1.9248c14599
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +5 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +11 -7
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/queue-import-history.js +40 -7
- package/dist/modules/communication_channels/commands/queue-import-history.js.map +2 -2
- package/dist/modules/communication_channels/lib/import-history-limits.js +69 -0
- package/dist/modules/communication_channels/lib/import-history-limits.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +36 -2
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +19 -1
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +6 -1
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +11 -7
- package/src/modules/communication_channels/commands/queue-import-history.ts +59 -11
- package/src/modules/communication_channels/i18n/de.json +2 -2
- package/src/modules/communication_channels/i18n/en.json +2 -2
- package/src/modules/communication_channels/i18n/es.json +2 -2
- package/src/modules/communication_channels/i18n/ko.json +2 -2
- package/src/modules/communication_channels/i18n/pl.json +2 -2
- package/src/modules/communication_channels/lib/adapter.ts +8 -2
- package/src/modules/communication_channels/lib/import-history-limits.ts +109 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +46 -5
- package/src/modules/query_index/lib/engine.ts +23 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -6,6 +6,7 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
|
|
|
6
6
|
import { CommunicationChannel } from "../../../../../data/entities.js";
|
|
7
7
|
import { ChannelAccessDeniedError, assertCanManageChannel } from "../../../../../lib/access-control.js";
|
|
8
8
|
import {
|
|
9
|
+
getQueueImportHistorySchema,
|
|
9
10
|
queueImportHistory,
|
|
10
11
|
queueImportHistorySchema
|
|
11
12
|
} from "../../../../../commands/queue-import-history.js";
|
|
@@ -20,6 +21,9 @@ const metadata = {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
const bodySchema = queueImportHistorySchema.omit({ channelId: true });
|
|
24
|
+
function buildBodySchema() {
|
|
25
|
+
return getQueueImportHistorySchema().omit({ channelId: true });
|
|
26
|
+
}
|
|
23
27
|
async function POST(req, context) {
|
|
24
28
|
const { id } = await context.params;
|
|
25
29
|
if (!z.string().uuid().safeParse(id).success) {
|
|
@@ -36,7 +40,7 @@ async function POST(req, context) {
|
|
|
36
40
|
} catch {
|
|
37
41
|
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
38
42
|
}
|
|
39
|
-
const parsed =
|
|
43
|
+
const parsed = buildBodySchema().safeParse(rawBody);
|
|
40
44
|
if (!parsed.success) {
|
|
41
45
|
const first = parsed.error.issues[0];
|
|
42
46
|
return NextResponse.json(
|
package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/communication_channels/api/post/channels/%5Bid%5D/import-history/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 } from '@open-mercato/shared/lib/encryption/find'\nimport type { CrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { CommunicationChannel } from '../../../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../../../lib/access-control'\nimport {\n queueImportHistory,\n queueImportHistorySchema,\n} from '../../../../../commands/queue-import-history'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'import-history' })\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\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import.\n *\n * Enqueues a `channel-import-history` job that calls\n * `adapter.importHistory(...)` paginated up to `maxMessages`, routes each\n * message through `ingest-inbound-message`, and reports progress on a\n * `ProgressJob` consumed by the existing ProgressTopBar.\n *\n * Owner self-service: a user may import history for their OWN mailbox (gated by\n * `connect_user_channel`). Importing into a shared/tenant-wide channel still\n * requires `communication_channels.channel.import_history` \u2014 enforced per\n * channel type by `assertCanManageChannel` in the handler.\n */\nexport const metadata = {\n path: '/communication_channels/channels/[id]/import-history',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst bodySchema = queueImportHistorySchema.omit({ channelId: true })\n\nexport async function POST(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?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // Parse body defensively \u2014 the route is called from a CrudForm dialog, but\n // also from automated tests / scripts.\n let rawBody: unknown\n try {\n const text = await req.text()\n rawBody = text ? JSON.parse(text) : {}\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed =
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,6BAA6B;AAEtC,SAAS,4BAA4B;AACrC,SAAS,0BAA0B,8BAA8B;AACjE;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAC3C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,iBAAiB,CAAC;AAsBpF,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,iBAAiB,CAAC,6CAA6C;AAAA,EACjE;AACF;AAMA,MAAM,aAAa,yBAAyB,KAAK,EAAE,WAAW,KAAK,CAAC;AAEpE,eAAsB,KAAK,KAAc,SAA0C;AACjF,QAAM,EAAE,GAAG,IAAI,MAAM,QAAQ;AAC7B,MAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,SAAS;AAC5C,WAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3E;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAIA,MAAI;AACJ,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,cAAU,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,EACvC,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1E;AACA,QAAM,SAAS,
|
|
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 } from '@open-mercato/shared/lib/encryption/find'\nimport type { CrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { CommunicationChannel } from '../../../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../../../lib/access-control'\nimport {\n getQueueImportHistorySchema,\n queueImportHistory,\n queueImportHistorySchema,\n} from '../../../../../commands/queue-import-history'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'import-history' })\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\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import.\n *\n * Enqueues a `channel-import-history` job that calls\n * `adapter.importHistory(...)` paginated up to `maxMessages`, routes each\n * message through `ingest-inbound-message`, and reports progress on a\n * `ProgressJob` consumed by the existing ProgressTopBar.\n *\n * Owner self-service: a user may import history for their OWN mailbox (gated by\n * `connect_user_channel`). Importing into a shared/tenant-wide channel still\n * requires `communication_channels.channel.import_history` \u2014 enforced per\n * channel type by `assertCanManageChannel` in the handler.\n */\nexport const metadata = {\n path: '/communication_channels/channels/[id]/import-history',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nconst bodySchema = queueImportHistorySchema.omit({ channelId: true })\n\nfunction buildBodySchema() {\n return getQueueImportHistorySchema().omit({ channelId: true })\n}\n\nexport async function POST(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?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n // Parse body defensively \u2014 the route is called from a CrudForm dialog, but\n // also from automated tests / scripts.\n let rawBody: unknown\n try {\n const text = await req.text()\n rawBody = text ? JSON.parse(text) : {}\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const parsed = buildBodySchema().safeParse(rawBody)\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n return NextResponse.json(\n {\n error: first?.message ?? 'Invalid request',\n fieldErrors: first ? { [first.path.join('.')]: first.message } : undefined,\n },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'No organization scope' }, { status: 400 })\n }\n const dscope = { tenantId: auth.tenantId as string, organizationId }\n\n // Per-user access guard \u2014 load the channel just to check ownership; the\n // command repeats the lookup but at the cost of one extra read we get a\n // clean 404 for non-owners (instead of a generic 500 from inside the cmd).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id,\n tenantId: auth.tenantId as string,\n organizationId,\n deletedAt: null,\n },\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,\n })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanManageChannel(\n { userId: (channel as { userId?: string | null }).userId },\n auth.sub as string,\n userFeatures,\n 'communication_channels.channel.import_history',\n )\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 const guard = await validateRouteMutationGuard({\n container,\n req,\n auth,\n input: {\n resourceKind: 'communication_channels.channel',\n resourceId: id,\n operation: 'custom',\n mutationPayload: parsed.data as unknown as Record<string, unknown>,\n },\n })\n if ('response' in guard) return guard.response\n\n try {\n const result = await queueImportHistory({\n container,\n scope: {\n tenantId: auth.tenantId as string,\n organizationId,\n userId: auth.sub as string,\n },\n input: { channelId: id, ...parsed.data },\n })\n await guard.afterSuccess()\n // 202 Accepted: durable work runs in the channel-import-history worker; the\n // response carries progressJobId for the ProgressTopBar (progress module contract).\n return NextResponse.json({ ok: true, ...result }, { status: 202 })\n } catch (err) {\n const candidate = err as CrudFormError\n if (candidate && typeof candidate.status === 'number') {\n return NextResponse.json(\n {\n error: candidate.message,\n fieldErrors: candidate.fieldErrors,\n },\n { status: candidate.status },\n )\n }\n logger.error('failed to enqueue for channel', { channelId: id, err })\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Failed to queue import-history' },\n { status: 500 },\n )\n }\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary: 'Queue a backlog import for a channel (Spec B \u00A7 Phase B6)',\n tags: ['CommunicationChannels'],\n requestBody: {\n required: true,\n contentType: 'application/json',\n schema: bodySchema,\n },\n responses: [\n { status: 202, description: 'Import job queued; returns { progressJobId }' },\n { status: 400, description: 'Invalid channel id or unsupported provider' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Channel not found / not accessible' },\n { status: 409, description: 'Channel is not connected (requires reauth / error)' },\n {\n status: 429,\n description: 'Another import is already running for this channel',\n },\n ],\n },\n },\n}\n\nexport default POST\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { extensionPoints } from "@open-mercato/core/modules/communication_channels/extension-points";
|
|
5
|
+
import { getImportHistoryLimits } from "@open-mercato/core/modules/communication_channels/lib/import-history-limits";
|
|
5
6
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
6
7
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
7
8
|
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
@@ -441,6 +442,7 @@ function ProfileCommunicationChannelsPage() {
|
|
|
441
442
|
}
|
|
442
443
|
function ImportHistoryDialog({ channel, onClose, onQueued }) {
|
|
443
444
|
const t = useT();
|
|
445
|
+
const importLimits = React.useMemo(() => getImportHistoryLimits(channel?.providerKey), [channel?.providerKey]);
|
|
444
446
|
const [sinceDays, setSinceDays] = React.useState("30");
|
|
445
447
|
const [contactEmails, setContactEmails] = React.useState("");
|
|
446
448
|
const [maxMessages, setMaxMessages] = React.useState("500");
|
|
@@ -464,16 +466,18 @@ function ImportHistoryDialog({ channel, onClose, onQueued }) {
|
|
|
464
466
|
const sinceNum = Number.parseInt(sinceDays, 10);
|
|
465
467
|
const maxNum = Number.parseInt(maxMessages, 10);
|
|
466
468
|
const errors = {};
|
|
467
|
-
if (!Number.isFinite(sinceNum) || sinceNum < 1 || sinceNum >
|
|
469
|
+
if (!Number.isFinite(sinceNum) || sinceNum < 1 || sinceNum > importLimits.maxSinceDays) {
|
|
468
470
|
errors.sinceDays = t(
|
|
469
471
|
"communication_channels.profile.importHistory.errors.sinceDays",
|
|
470
|
-
"Choose a number between 1 and
|
|
472
|
+
"Choose a number between 1 and {max} days.",
|
|
473
|
+
{ max: importLimits.maxSinceDays }
|
|
471
474
|
);
|
|
472
475
|
}
|
|
473
|
-
if (!Number.isFinite(maxNum) || maxNum < 1 || maxNum >
|
|
476
|
+
if (!Number.isFinite(maxNum) || maxNum < 1 || maxNum > importLimits.maxMessages) {
|
|
474
477
|
errors.maxMessages = t(
|
|
475
478
|
"communication_channels.profile.importHistory.errors.maxMessages",
|
|
476
|
-
"Choose a number between 1 and
|
|
479
|
+
"Choose a number between 1 and {max} messages.",
|
|
480
|
+
{ max: importLimits.maxMessages }
|
|
477
481
|
);
|
|
478
482
|
}
|
|
479
483
|
const parsedEmails = contactEmails.split(/[\s,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
@@ -545,7 +549,7 @@ function ImportHistoryDialog({ channel, onClose, onQueued }) {
|
|
|
545
549
|
"success"
|
|
546
550
|
);
|
|
547
551
|
onQueued();
|
|
548
|
-
}, [channel, sinceDays, maxMessages, contactEmails, submitting, t, onQueued, retryLastMutation, runMutation]);
|
|
552
|
+
}, [channel, sinceDays, maxMessages, contactEmails, submitting, t, onQueued, retryLastMutation, runMutation, importLimits]);
|
|
549
553
|
const handleKeyDown = React.useCallback(
|
|
550
554
|
(event) => {
|
|
551
555
|
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
@@ -574,7 +578,7 @@ function ImportHistoryDialog({ channel, onClose, onQueued }) {
|
|
|
574
578
|
id: "import-history-since",
|
|
575
579
|
type: "number",
|
|
576
580
|
min: 1,
|
|
577
|
-
max:
|
|
581
|
+
max: importLimits.maxSinceDays,
|
|
578
582
|
value: sinceDays,
|
|
579
583
|
onChange: (e) => setSinceDays(e.target.value),
|
|
580
584
|
"aria-invalid": Boolean(fieldErrors.sinceDays)
|
|
@@ -614,7 +618,7 @@ function ImportHistoryDialog({ channel, onClose, onQueued }) {
|
|
|
614
618
|
id: "import-history-max",
|
|
615
619
|
type: "number",
|
|
616
620
|
min: 1,
|
|
617
|
-
max:
|
|
621
|
+
max: importLimits.maxMessages,
|
|
618
622
|
value: maxMessages,
|
|
619
623
|
onChange: (e) => setMaxMessages(e.target.value),
|
|
620
624
|
"aria-invalid": Boolean(fieldErrors.maxMessages)
|
package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/communication_channels/backend/profile/communication-channels/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/communication_channels/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { apiCall } 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 { ConnectChannelMenu } from './ConnectChannelMenu'\n\ntype ChannelRow = {\n id: string\n providerKey: string\n channelType: string\n displayName: string\n externalIdentifier: string | null\n isPrimary: boolean\n isActive: boolean\n status: 'connected' | 'requires_reauth' | 'error' | 'disconnected'\n lastError: string | null\n pollIntervalSeconds: number | null\n lastPolledAt: string | null\n /** Spec C \u2014 push delivery state (null when provider doesn't support push). */\n pushStatus: 'active' | 'inactive' | 'failed' | null\n lastPushError: { code: string | null; message: string | null; at: string | null } | null\n /**\n * `true` when the adapter declares real-time push, so the hub's poll worker\n * skips this channel entirely \u2014 inbound arrives over the provider connection.\n */\n supportsRealtimePush: boolean\n /** `true` when the adapter implements `registerPush` (Gmail-style subscriptions). */\n supportsPushRegistration: boolean\n createdAt: string | null\n}\n\nconst PROFILE_CHANNELS_MUTATION_CONTEXT_ID = 'communication-channels-profile'\nconst IMPORT_HISTORY_MUTATION_CONTEXT_ID = 'communication-channels-import-history'\nconst DISCONNECT_MUTATION_CONTEXT_ID = 'communication-channels-disconnect'\n\ntype ChannelMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nexport default function ProfileCommunicationChannelsPage() {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n const flashType = searchParams?.get('flash')\n const flashCode = searchParams?.get('code')\n const flashProvider = searchParams?.get('provider')\n\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [reloadKey, setReloadKey] = React.useState(0)\n const [importChannel, setImportChannel] = React.useState<ChannelRow | null>(null)\n const [disconnectChannel, setDisconnectChannel] = React.useState<ChannelRow | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (flashType === 'connected') {\n flash(\n flashProvider\n ? t('communication_channels.profile.flash.connectedWithProvider', 'Channel connected ({provider}).', {\n provider: flashProvider,\n })\n : t('communication_channels.profile.flash.connected', 'Channel connected.'),\n 'success',\n )\n } else if (flashType === 'error') {\n flash(\n flashCode === 'oauth_client_not_configured'\n ? t(\n 'communication_channels.profile.connect.notConfigured',\n 'This provider is not configured yet. Ask an administrator to add the OAuth Client ID and Secret under Integrations before connecting a mailbox.',\n )\n : flashCode === 'mailbox_already_connected'\n ? t(\n 'communication_channels.profile.connect.mailboxAlreadyConnected',\n 'This mailbox is already connected through another provider. Disconnect it first to reconnect it with a different one.',\n )\n : flashCode\n ? t('communication_channels.profile.flash.errorWithCode', 'Failed to connect channel \u2014 {code}.', {\n code: flashCode,\n })\n : t('communication_channels.profile.flash.error', 'Failed to connect channel.'),\n 'error',\n )\n }\n }, [flashType, flashCode, flashProvider, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setErrorMessage(null)\n const response = await apiCall<{ items?: ChannelRow[] }>(\n '/api/communication_channels/me/channels',\n ).catch((err: unknown) => ({\n ok: false,\n result: { error: err instanceof Error ? err.message : 'Failed to load channels' },\n }))\n if (cancelled) return\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n setErrorMessage(\n body?.error ?? t('communication_channels.errors.loadList', 'Failed to load channels'),\n )\n setRows([])\n } else {\n const data = (response.result ?? {}) as { items?: ChannelRow[] }\n setRows(Array.isArray(data.items) ? data.items : [])\n }\n setIsLoading(false)\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [reloadKey, t])\n\n const reauthRows = rows.filter((r) => r.status === 'requires_reauth')\n\n const reloadChannels = React.useCallback(() => setReloadKey((k) => k + 1), [])\n\n const onSetPrimary = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/set-primary`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { isPrimary: true },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.profile.actions.setPrimaryFailed', 'Failed to set as primary'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ?? t('communication_channels.profile.actions.setPrimaryFailed', 'Failed to set as primary'),\n 'error',\n )\n return\n }\n flash(\n t('communication_channels.profile.actions.setPrimarySuccess', 'Marked as primary.'),\n 'success',\n )\n setReloadKey((k) => k + 1)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const onRegisterPush = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall<{ pushStatus?: string; error?: { code: string; message: string } }>(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/push/register`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { action: 'push-register' },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.push.button.reregister', 'Re-register push'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(body?.error ?? t('communication_channels.push.flash.registerFailed', 'Failed to register push'), 'error')\n return\n }\n const result = (response.result ?? {}) as { pushStatus?: string }\n if (result.pushStatus === 'active') {\n flash(t('communication_channels.push.status.active', 'Push active'), 'success')\n } else {\n flash(\n t(\n 'communication_channels.push.status.failed',\n 'Push registration returned a non-active status \u2014 falling back to polling.',\n ),\n 'error',\n )\n }\n setReloadKey((k) => k + 1)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const onPollNow = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/poll-now`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { action: 'poll-now' },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.profile.actions.pollNowFailed', 'Failed to trigger poll'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ?? t('communication_channels.profile.actions.pollNowFailed', 'Failed to trigger poll'),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.actions.pollNowSuccess',\n 'Poll triggered \u2014 new messages will appear on linked Person timelines in a few seconds.',\n ),\n 'success',\n )\n // Give the worker a moment to fetch + ingest, then refetch our channel list\n // so `lastPolledAt` updates in the UI.\n setTimeout(() => setReloadKey((k) => k + 1), 1500)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(\n () => [\n {\n header: t('communication_channels.columns.displayName', 'Channel'),\n accessorKey: 'displayName',\n },\n {\n header: t('communication_channels.columns.provider', 'Provider'),\n accessorKey: 'providerKey',\n cell: ({ row }) => (\n <Tag variant=\"info\">\n {t(\n `communication_channels.channel.providers.${row.original.providerKey}`,\n row.original.providerKey,\n )}\n </Tag>\n ),\n },\n {\n header: t('communication_channels.columns.identifier', 'Email / username'),\n accessorKey: 'externalIdentifier',\n cell: ({ row }) => row.original.externalIdentifier ?? '\u2014',\n meta: { truncate: true, maxWidth: 240 },\n },\n {\n header: t('communication_channels.profile.columns.primary', 'Primary'),\n accessorKey: 'isPrimary',\n cell: ({ row }) =>\n row.original.isPrimary ? (\n <Tag variant=\"success\" dot>\n {t('communication_channels.profile.primary', 'Primary')}\n </Tag>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onSetPrimary(row.original.id)}\n aria-label={t('communication_channels.profile.actions.setPrimary', 'Set as primary')}\n >\n {t('communication_channels.profile.actions.setPrimary', 'Set as primary')}\n </Button>\n ),\n },\n {\n header: t('communication_channels.columns.status', 'Status'),\n accessorKey: 'status',\n cell: ({ row }) => statusTag(row.original.status, t),\n },\n {\n id: 'pushStatus',\n // Its own key: sharing `push.status.active` made the header render\n // \"Push active\" over a column whose rows say \"Polling only\" (#4980).\n header: t('communication_channels.profile.columns.push', 'Push'),\n cell: ({ row }) => {\n const ps = row.original.pushStatus\n const errorTitle = row.original.lastPushError?.message ?? undefined\n // Derived from the adapter's declared capabilities, never from the\n // provider name: `supportsPushRegistration` means push subscriptions\n // can be (re-)registered from here, `supportsRealtimePush` means the\n // hub's poll worker skips the channel because the provider connection\n // delivers inbound itself (#4980).\n if (!row.original.supportsPushRegistration) {\n if (!row.original.supportsRealtimePush) {\n return (\n <span className=\"text-xs text-muted-foreground\">\n {t('communication_channels.push.status.inactive', 'Polling only')}\n </span>\n )\n }\n if (ps === 'failed') {\n return (\n <Tag variant=\"error\" dot title={errorTitle}>\n {t('communication_channels.push.status.pushDrivenFailed', 'Push connection failed')}\n </Tag>\n )\n }\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.push.status.pushDriven', 'Push-driven')}\n </Tag>\n )\n }\n if (ps === 'active') {\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.push.status.active', 'Push active')}\n </Tag>\n )\n }\n if (ps === 'failed') {\n return (\n <div className=\"flex items-center gap-2\">\n <Tag variant=\"error\" dot>\n {t('communication_channels.push.status.failed', 'Push failed \u2014 using polling')}\n </Tag>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onRegisterPush(row.original.id)}\n aria-label={t('communication_channels.push.button.reregister', 'Re-register push')}\n title={errorTitle}\n >\n {t('communication_channels.push.button.reregister', 'Re-register push')}\n </Button>\n </div>\n )\n }\n // null or 'inactive' \u2014 the provider can register push but has not yet.\n // Only a hub-polled channel falls back to polling meanwhile; for a\n // push-driven one nothing is delivering inbound at all, so claiming\n // \"Polling only\" would repeat the defect this issue is about (#4980).\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {row.original.supportsRealtimePush\n ? t('communication_channels.push.status.notRegistered', 'Push not registered')\n : t('communication_channels.push.status.inactive', 'Polling only')}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onRegisterPush(row.original.id)}\n aria-label={t('communication_channels.push.button.reregister', 'Re-register push')}\n >\n {t('communication_channels.push.button.reregister', 'Re-register push')}\n </Button>\n </div>\n )\n },\n },\n {\n header: t('communication_channels.profile.columns.lastPolled', 'Last synced'),\n accessorKey: 'lastPolledAt',\n cell: ({ row }) =>\n row.original.lastPolledAt\n ? new Date(row.original.lastPolledAt).toLocaleString()\n : '\u2014',\n },\n {\n id: 'importHistory',\n header: t('communication_channels.profile.columns.importHistory', 'History'),\n cell: ({ row }) => {\n const eligible =\n row.original.isActive &&\n row.original.status === 'connected' &&\n row.original.channelType === 'email'\n const label = t('communication_channels.profile.actions.importHistory', 'Import history')\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setImportChannel(row.original)}\n disabled={!eligible}\n aria-label={label}\n >\n {label}\n </Button>\n )\n },\n },\n {\n id: 'pollNow',\n header: t('communication_channels.profile.columns.pollNow', 'Sync'),\n cell: ({ row }) => {\n // Allowed from 'connected' AND 'error' \u2014 the latter lets the user\n // recover a stuck channel without disconnecting + reconnecting.\n // 'requires_reauth' and 'disconnected' are owned by other flows.\n // A push-driven channel is never polled by the worker, so offering the\n // action at all would promise a sync that cannot happen (#4980).\n const pushDriven = row.original.supportsRealtimePush\n const pollable =\n row.original.isActive &&\n !pushDriven &&\n (row.original.status === 'connected' || row.original.status === 'error')\n const label =\n row.original.status === 'error'\n ? t('communication_channels.profile.actions.retryPoll', 'Retry')\n : t('communication_channels.profile.actions.pollNow', 'Poll now')\n const disabledReason = pushDriven\n ? t(\n 'communication_channels.profile.actions.pollNowPushDriven',\n 'This channel is push-driven \u2014 inbound messages arrive over the provider connection, so polling does not apply.',\n )\n : undefined\n const button = (\n <Button\n type=\"button\"\n variant={row.original.status === 'error' ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => void onPollNow(row.original.id)}\n disabled={!pollable}\n aria-label={disabledReason ? `${label} \u2014 ${disabledReason}` : label}\n >\n {label}\n </Button>\n )\n // A disabled button does not receive hover events in every browser, so\n // the explanation lives on a wrapper the pointer can still reach.\n return disabledReason ? <span title={disabledReason}>{button}</span> : button\n },\n },\n {\n id: 'disconnect',\n header: t('communication_channels.profile.columns.disconnect', 'Connection'),\n cell: ({ row }) => {\n const label = t('communication_channels.profile.actions.disconnect', 'Disconnect')\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setDisconnectChannel(row.original)}\n aria-label={label}\n >\n {label}\n </Button>\n )\n },\n },\n ],\n [onSetPrimary, onPollNow, onRegisterPush, t],\n )\n\n return (\n <Page>\n <PageBody>\n <header className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold\">\n {t('communication_channels.profile.title', 'My communication channels')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'communication_channels.profile.subtitle',\n 'Connect your communication channels so outbound messages come from your own account and inbound messages land in your unified inbox.',\n )}\n </p>\n </div>\n {/* Provider connect entry points injected by each channel-* package\n (channel-gmail, channel-imap) via UMES. They stack inside one\n dropdown so the header does not widen per installed provider. */}\n <ConnectChannelMenu onConnected={reloadChannels} />\n </header>\n\n {reauthRows.length > 0 ? (\n <Alert status=\"warning\" className=\"mb-4\">\n <AlertDescription>\n {t(\n 'communication_channels.profile.alerts.requiresReauth',\n '{count} channel(s) need reconnection. Click \"Reconnect\" on the affected channel below.',\n { count: reauthRows.length },\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n\n <DataTable<ChannelRow>\n title={t('communication_channels.profile.tableTitle', 'Your channels')}\n titleHeadingLevel={2}\n extensionTableId={extensionPoints.hosts.profileChannelsTable.tableId}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n error={errorMessage}\n emptyState={t(\n 'communication_channels.profile.empty',\n 'You have no connected channels yet. Add one using the menu at the top of this page.',\n )}\n />\n <ImportHistoryDialog\n channel={importChannel}\n onClose={() => setImportChannel(null)}\n onQueued={() => {\n setImportChannel(null)\n router.refresh()\n }}\n />\n <DisconnectChannelDialog\n channel={disconnectChannel}\n onClose={() => setDisconnectChannel(null)}\n onDisconnected={() => {\n setDisconnectChannel(null)\n setReloadKey((k) => k + 1)\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype ImportHistoryDialogProps = {\n channel: ChannelRow | null\n onClose: () => void\n onQueued: () => void\n}\n\nfunction ImportHistoryDialog({ channel, onClose, onQueued }: ImportHistoryDialogProps): React.JSX.Element {\n const t = useT()\n const [sinceDays, setSinceDays] = React.useState('30')\n const [contactEmails, setContactEmails] = React.useState('')\n const [maxMessages, setMaxMessages] = React.useState('500')\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const [submitting, setSubmitting] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: IMPORT_HISTORY_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (channel) {\n setSinceDays('30')\n setContactEmails('')\n setMaxMessages('500')\n setFieldErrors({})\n setSubmitting(false)\n }\n }, [channel?.id])\n\n const handleSubmit = React.useCallback(async () => {\n if (!channel || submitting) return\n const sinceNum = Number.parseInt(sinceDays, 10)\n const maxNum = Number.parseInt(maxMessages, 10)\n const errors: Record<string, string> = {}\n if (!Number.isFinite(sinceNum) || sinceNum < 1 || sinceNum > 365) {\n errors.sinceDays = t(\n 'communication_channels.profile.importHistory.errors.sinceDays',\n 'Choose a number between 1 and 365 days.',\n )\n }\n if (!Number.isFinite(maxNum) || maxNum < 1 || maxNum > 5000) {\n errors.maxMessages = t(\n 'communication_channels.profile.importHistory.errors.maxMessages',\n 'Choose a number between 1 and 5000 messages.',\n )\n }\n const parsedEmails = contactEmails\n .split(/[\\s,;]+/)\n .map((s) => s.trim())\n .filter(Boolean)\n if (parsedEmails.length > 0 && parsedEmails.some((s) => !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s))) {\n errors.contactEmails = t(\n 'communication_channels.profile.importHistory.errors.contactEmails',\n 'One or more entries is not a valid email address.',\n )\n }\n if (Object.keys(errors).length > 0) {\n setFieldErrors(errors)\n return\n }\n setFieldErrors({})\n setSubmitting(true)\n const mutationPayload = {\n sinceDays: sinceNum,\n maxMessages: maxNum,\n ...(parsedEmails.length > 0 ? { contactEmails: parsedEmails } : {}),\n }\n let response\n try {\n response = await runMutation({\n operation: () => apiCall<{ progressJobId?: string }>(\n `/api/communication_channels/channels/${encodeURIComponent(channel.id)}/import-history`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(mutationPayload),\n },\n ),\n context: {\n formId: IMPORT_HISTORY_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channel.id,\n retryLastMutation,\n },\n mutationPayload,\n })\n } catch (err) {\n setSubmitting(false)\n flash(\n err instanceof Error\n ? err.message\n : t('communication_channels.profile.importHistory.flash.error', 'Failed to queue history import.'),\n 'error',\n )\n return\n }\n setSubmitting(false)\n if (!response.ok) {\n const body = response.result as { error?: string; fieldErrors?: Record<string, string> } | undefined\n if (body?.fieldErrors && Object.keys(body.fieldErrors).length > 0) {\n setFieldErrors(body.fieldErrors)\n return\n }\n flash(\n body?.error ??\n t(\n 'communication_channels.profile.importHistory.flash.error',\n 'Failed to queue history import.',\n ),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.importHistory.flash.success',\n 'History import queued \u2014 track progress in the top bar.',\n ),\n 'success',\n )\n onQueued()\n }, [channel, sinceDays, maxMessages, contactEmails, submitting, t, onQueued, retryLastMutation, runMutation])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <Dialog open={channel !== null} onOpenChange={(open) => { if (!open) onClose() }}>\n <DialogContent onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.importHistory.title', 'Import channel history')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'communication_channels.profile.importHistory.description',\n 'Pull older messages this channel never observed at connect-time. Filters narrow the search server-side.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-since\">\n {t('communication_channels.profile.importHistory.fields.sinceDays', 'Look back (days)')}\n </Label>\n <Input\n id=\"import-history-since\"\n type=\"number\"\n min={1}\n max={365}\n value={sinceDays}\n onChange={(e) => setSinceDays(e.target.value)}\n aria-invalid={Boolean(fieldErrors.sinceDays)}\n />\n {fieldErrors.sinceDays ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.sinceDays}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-emails\">\n {t(\n 'communication_channels.profile.importHistory.fields.contactEmails',\n 'Filter by sender (optional)',\n )}\n </Label>\n <Textarea\n id=\"import-history-emails\"\n rows={3}\n value={contactEmails}\n onChange={(e) => setContactEmails(e.target.value)}\n placeholder={t(\n 'communication_channels.profile.importHistory.fields.contactEmailsPlaceholder',\n 'alice@example.com, bob@example.com',\n )}\n aria-invalid={Boolean(fieldErrors.contactEmails)}\n />\n {fieldErrors.contactEmails ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.contactEmails}</p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'communication_channels.profile.importHistory.fields.contactEmailsHint',\n 'Leave empty to scan all senders in the window.',\n )}\n </p>\n )}\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-max\">\n {t('communication_channels.profile.importHistory.fields.maxMessages', 'Maximum messages')}\n </Label>\n <Input\n id=\"import-history-max\"\n type=\"number\"\n min={1}\n max={5000}\n value={maxMessages}\n onChange={(e) => setMaxMessages(e.target.value)}\n aria-invalid={Boolean(fieldErrors.maxMessages)}\n />\n {fieldErrors.maxMessages ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.maxMessages}</p>\n ) : null}\n </div>\n\n {fieldErrors.channelId ? (\n <Alert status=\"warning\">\n <AlertDescription>{fieldErrors.channelId}</AlertDescription>\n </Alert>\n ) : null}\n </div>\n\n <DialogFooter>\n <span className=\"mr-auto text-xs text-muted-foreground\">\n <KbdShortcut keys={['\u2318', 'Enter']} />\n </span>\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={submitting}>\n {t('communication_channels.profile.importHistory.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSubmit()} disabled={submitting}>\n {submitting\n ? t('communication_channels.profile.importHistory.submitting', 'Queueing\u2026')\n : t('communication_channels.profile.importHistory.submit', 'Start import')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\ntype DisconnectChannelDialogProps = {\n channel: ChannelRow | null\n onClose: () => void\n onDisconnected: () => void\n}\n\nfunction DisconnectChannelDialog({\n channel,\n onClose,\n onDisconnected,\n}: DisconnectChannelDialogProps): React.JSX.Element {\n const t = useT()\n const [submitting, setSubmitting] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: DISCONNECT_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (channel) setSubmitting(false)\n }, [channel?.id])\n\n const handleConfirm = React.useCallback(async () => {\n if (!channel || submitting) return\n setSubmitting(true)\n let response\n try {\n response = await runMutation({\n // optimistic-lock-exempt: self-service connect/disconnect of the\n // signed-in operator's OWN communication channel (an integration link),\n // not a shared multi-editor record. Disconnect is a terminal action\n // keyed by channel id; there is no concurrent-edit lost-update window to\n // guard, and the row carries no client-surfaced `updatedAt` round-trip.\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channel.id)}`,\n { method: 'DELETE' },\n ),\n context: {\n formId: DISCONNECT_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channel.id,\n retryLastMutation,\n },\n mutationPayload: { action: 'disconnect' },\n })\n } catch (err) {\n setSubmitting(false)\n flash(\n err instanceof Error\n ? err.message\n : t('communication_channels.profile.actions.disconnectFailed', 'Failed to disconnect channel'),\n 'error',\n )\n return\n }\n setSubmitting(false)\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ??\n t('communication_channels.profile.actions.disconnectFailed', 'Failed to disconnect channel'),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.actions.disconnectSuccess',\n 'Channel disconnected. You can reconnect it anytime.',\n ),\n 'success',\n )\n onDisconnected()\n }, [channel, submitting, runMutation, retryLastMutation, t, onDisconnected])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n void handleConfirm()\n }\n },\n [handleConfirm],\n )\n\n return (\n <Dialog open={channel !== null} onOpenChange={(open) => { if (!open) onClose() }}>\n <DialogContent onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.disconnect.title', 'Disconnect channel')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'communication_channels.profile.disconnect.description',\n 'This removes the connection and stops syncing. Emails already imported stay on your timelines. You can reconnect anytime.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n {channel ? (\n <p className=\"text-sm font-medium\">{channel.externalIdentifier ?? channel.displayName}</p>\n ) : null}\n\n <DialogFooter>\n <span className=\"mr-auto text-xs text-muted-foreground\">\n <KbdShortcut keys={['\u2318', 'Enter']} />\n </span>\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={submitting}>\n {t('communication_channels.profile.disconnect.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" variant=\"destructive-solid\" onClick={() => void handleConfirm()} disabled={submitting}>\n {submitting\n ? t('communication_channels.profile.disconnect.submitting', 'Disconnecting\u2026')\n : t('communication_channels.profile.disconnect.confirm', 'Disconnect')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction statusTag(\n status: ChannelRow['status'],\n t: (key: string, fallback?: string) => string,\n): React.ReactNode {\n switch (status) {\n case 'connected':\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.status.connected', 'Connected')}\n </Tag>\n )\n case 'requires_reauth':\n return (\n <Tag variant=\"warning\" dot>\n {t('communication_channels.status.requiresReauth', 'Needs reconnection')}\n </Tag>\n )\n case 'error':\n return (\n <Tag variant=\"error\" dot>\n {t('communication_channels.status.error', 'Error')}\n </Tag>\n )\n case 'disconnected':\n return <Tag variant=\"neutral\">{t('communication_channels.status.disconnected', 'Disconnected')}</Tag>\n default:\n return <Tag variant=\"neutral\">{status}</Tag>\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/communication_channels/extension-points'\nimport { getImportHistoryLimits } from '@open-mercato/core/modules/communication_channels/lib/import-history-limits'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { apiCall } 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 { ConnectChannelMenu } from './ConnectChannelMenu'\n\ntype ChannelRow = {\n id: string\n providerKey: string\n channelType: string\n displayName: string\n externalIdentifier: string | null\n isPrimary: boolean\n isActive: boolean\n status: 'connected' | 'requires_reauth' | 'error' | 'disconnected'\n lastError: string | null\n pollIntervalSeconds: number | null\n lastPolledAt: string | null\n /** Spec C \u2014 push delivery state (null when provider doesn't support push). */\n pushStatus: 'active' | 'inactive' | 'failed' | null\n lastPushError: { code: string | null; message: string | null; at: string | null } | null\n /**\n * `true` when the adapter declares real-time push, so the hub's poll worker\n * skips this channel entirely \u2014 inbound arrives over the provider connection.\n */\n supportsRealtimePush: boolean\n /** `true` when the adapter implements `registerPush` (Gmail-style subscriptions). */\n supportsPushRegistration: boolean\n createdAt: string | null\n}\n\nconst PROFILE_CHANNELS_MUTATION_CONTEXT_ID = 'communication-channels-profile'\nconst IMPORT_HISTORY_MUTATION_CONTEXT_ID = 'communication-channels-import-history'\nconst DISCONNECT_MUTATION_CONTEXT_ID = 'communication-channels-disconnect'\n\ntype ChannelMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nexport default function ProfileCommunicationChannelsPage() {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n const flashType = searchParams?.get('flash')\n const flashCode = searchParams?.get('code')\n const flashProvider = searchParams?.get('provider')\n\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [reloadKey, setReloadKey] = React.useState(0)\n const [importChannel, setImportChannel] = React.useState<ChannelRow | null>(null)\n const [disconnectChannel, setDisconnectChannel] = React.useState<ChannelRow | null>(null)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (flashType === 'connected') {\n flash(\n flashProvider\n ? t('communication_channels.profile.flash.connectedWithProvider', 'Channel connected ({provider}).', {\n provider: flashProvider,\n })\n : t('communication_channels.profile.flash.connected', 'Channel connected.'),\n 'success',\n )\n } else if (flashType === 'error') {\n flash(\n flashCode === 'oauth_client_not_configured'\n ? t(\n 'communication_channels.profile.connect.notConfigured',\n 'This provider is not configured yet. Ask an administrator to add the OAuth Client ID and Secret under Integrations before connecting a mailbox.',\n )\n : flashCode === 'mailbox_already_connected'\n ? t(\n 'communication_channels.profile.connect.mailboxAlreadyConnected',\n 'This mailbox is already connected through another provider. Disconnect it first to reconnect it with a different one.',\n )\n : flashCode\n ? t('communication_channels.profile.flash.errorWithCode', 'Failed to connect channel \u2014 {code}.', {\n code: flashCode,\n })\n : t('communication_channels.profile.flash.error', 'Failed to connect channel.'),\n 'error',\n )\n }\n }, [flashType, flashCode, flashProvider, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setErrorMessage(null)\n const response = await apiCall<{ items?: ChannelRow[] }>(\n '/api/communication_channels/me/channels',\n ).catch((err: unknown) => ({\n ok: false,\n result: { error: err instanceof Error ? err.message : 'Failed to load channels' },\n }))\n if (cancelled) return\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n setErrorMessage(\n body?.error ?? t('communication_channels.errors.loadList', 'Failed to load channels'),\n )\n setRows([])\n } else {\n const data = (response.result ?? {}) as { items?: ChannelRow[] }\n setRows(Array.isArray(data.items) ? data.items : [])\n }\n setIsLoading(false)\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [reloadKey, t])\n\n const reauthRows = rows.filter((r) => r.status === 'requires_reauth')\n\n const reloadChannels = React.useCallback(() => setReloadKey((k) => k + 1), [])\n\n const onSetPrimary = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/set-primary`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { isPrimary: true },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.profile.actions.setPrimaryFailed', 'Failed to set as primary'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ?? t('communication_channels.profile.actions.setPrimaryFailed', 'Failed to set as primary'),\n 'error',\n )\n return\n }\n flash(\n t('communication_channels.profile.actions.setPrimarySuccess', 'Marked as primary.'),\n 'success',\n )\n setReloadKey((k) => k + 1)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const onRegisterPush = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall<{ pushStatus?: string; error?: { code: string; message: string } }>(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/push/register`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { action: 'push-register' },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.push.button.reregister', 'Re-register push'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(body?.error ?? t('communication_channels.push.flash.registerFailed', 'Failed to register push'), 'error')\n return\n }\n const result = (response.result ?? {}) as { pushStatus?: string }\n if (result.pushStatus === 'active') {\n flash(t('communication_channels.push.status.active', 'Push active'), 'success')\n } else {\n flash(\n t(\n 'communication_channels.push.status.failed',\n 'Push registration returned a non-active status \u2014 falling back to polling.',\n ),\n 'error',\n )\n }\n setReloadKey((k) => k + 1)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const onPollNow = React.useCallback(\n async (channelId: string) => {\n let response\n try {\n response = await runMutation({\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channelId)}/poll-now`,\n { method: 'POST' },\n ),\n context: {\n formId: PROFILE_CHANNELS_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channelId,\n retryLastMutation,\n },\n mutationPayload: { action: 'poll-now' },\n })\n } catch (err) {\n flash(err instanceof Error ? err.message : t('communication_channels.profile.actions.pollNowFailed', 'Failed to trigger poll'), 'error')\n return\n }\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ?? t('communication_channels.profile.actions.pollNowFailed', 'Failed to trigger poll'),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.actions.pollNowSuccess',\n 'Poll triggered \u2014 new messages will appear on linked Person timelines in a few seconds.',\n ),\n 'success',\n )\n // Give the worker a moment to fetch + ingest, then refetch our channel list\n // so `lastPolledAt` updates in the UI.\n setTimeout(() => setReloadKey((k) => k + 1), 1500)\n },\n [retryLastMutation, runMutation, t],\n )\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(\n () => [\n {\n header: t('communication_channels.columns.displayName', 'Channel'),\n accessorKey: 'displayName',\n },\n {\n header: t('communication_channels.columns.provider', 'Provider'),\n accessorKey: 'providerKey',\n cell: ({ row }) => (\n <Tag variant=\"info\">\n {t(\n `communication_channels.channel.providers.${row.original.providerKey}`,\n row.original.providerKey,\n )}\n </Tag>\n ),\n },\n {\n header: t('communication_channels.columns.identifier', 'Email / username'),\n accessorKey: 'externalIdentifier',\n cell: ({ row }) => row.original.externalIdentifier ?? '\u2014',\n meta: { truncate: true, maxWidth: 240 },\n },\n {\n header: t('communication_channels.profile.columns.primary', 'Primary'),\n accessorKey: 'isPrimary',\n cell: ({ row }) =>\n row.original.isPrimary ? (\n <Tag variant=\"success\" dot>\n {t('communication_channels.profile.primary', 'Primary')}\n </Tag>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onSetPrimary(row.original.id)}\n aria-label={t('communication_channels.profile.actions.setPrimary', 'Set as primary')}\n >\n {t('communication_channels.profile.actions.setPrimary', 'Set as primary')}\n </Button>\n ),\n },\n {\n header: t('communication_channels.columns.status', 'Status'),\n accessorKey: 'status',\n cell: ({ row }) => statusTag(row.original.status, t),\n },\n {\n id: 'pushStatus',\n // Its own key: sharing `push.status.active` made the header render\n // \"Push active\" over a column whose rows say \"Polling only\" (#4980).\n header: t('communication_channels.profile.columns.push', 'Push'),\n cell: ({ row }) => {\n const ps = row.original.pushStatus\n const errorTitle = row.original.lastPushError?.message ?? undefined\n // Derived from the adapter's declared capabilities, never from the\n // provider name: `supportsPushRegistration` means push subscriptions\n // can be (re-)registered from here, `supportsRealtimePush` means the\n // hub's poll worker skips the channel because the provider connection\n // delivers inbound itself (#4980).\n if (!row.original.supportsPushRegistration) {\n if (!row.original.supportsRealtimePush) {\n return (\n <span className=\"text-xs text-muted-foreground\">\n {t('communication_channels.push.status.inactive', 'Polling only')}\n </span>\n )\n }\n if (ps === 'failed') {\n return (\n <Tag variant=\"error\" dot title={errorTitle}>\n {t('communication_channels.push.status.pushDrivenFailed', 'Push connection failed')}\n </Tag>\n )\n }\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.push.status.pushDriven', 'Push-driven')}\n </Tag>\n )\n }\n if (ps === 'active') {\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.push.status.active', 'Push active')}\n </Tag>\n )\n }\n if (ps === 'failed') {\n return (\n <div className=\"flex items-center gap-2\">\n <Tag variant=\"error\" dot>\n {t('communication_channels.push.status.failed', 'Push failed \u2014 using polling')}\n </Tag>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onRegisterPush(row.original.id)}\n aria-label={t('communication_channels.push.button.reregister', 'Re-register push')}\n title={errorTitle}\n >\n {t('communication_channels.push.button.reregister', 'Re-register push')}\n </Button>\n </div>\n )\n }\n // null or 'inactive' \u2014 the provider can register push but has not yet.\n // Only a hub-polled channel falls back to polling meanwhile; for a\n // push-driven one nothing is delivering inbound at all, so claiming\n // \"Polling only\" would repeat the defect this issue is about (#4980).\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {row.original.supportsRealtimePush\n ? t('communication_channels.push.status.notRegistered', 'Push not registered')\n : t('communication_channels.push.status.inactive', 'Polling only')}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void onRegisterPush(row.original.id)}\n aria-label={t('communication_channels.push.button.reregister', 'Re-register push')}\n >\n {t('communication_channels.push.button.reregister', 'Re-register push')}\n </Button>\n </div>\n )\n },\n },\n {\n header: t('communication_channels.profile.columns.lastPolled', 'Last synced'),\n accessorKey: 'lastPolledAt',\n cell: ({ row }) =>\n row.original.lastPolledAt\n ? new Date(row.original.lastPolledAt).toLocaleString()\n : '\u2014',\n },\n {\n id: 'importHistory',\n header: t('communication_channels.profile.columns.importHistory', 'History'),\n cell: ({ row }) => {\n const eligible =\n row.original.isActive &&\n row.original.status === 'connected' &&\n row.original.channelType === 'email'\n const label = t('communication_channels.profile.actions.importHistory', 'Import history')\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setImportChannel(row.original)}\n disabled={!eligible}\n aria-label={label}\n >\n {label}\n </Button>\n )\n },\n },\n {\n id: 'pollNow',\n header: t('communication_channels.profile.columns.pollNow', 'Sync'),\n cell: ({ row }) => {\n // Allowed from 'connected' AND 'error' \u2014 the latter lets the user\n // recover a stuck channel without disconnecting + reconnecting.\n // 'requires_reauth' and 'disconnected' are owned by other flows.\n // A push-driven channel is never polled by the worker, so offering the\n // action at all would promise a sync that cannot happen (#4980).\n const pushDriven = row.original.supportsRealtimePush\n const pollable =\n row.original.isActive &&\n !pushDriven &&\n (row.original.status === 'connected' || row.original.status === 'error')\n const label =\n row.original.status === 'error'\n ? t('communication_channels.profile.actions.retryPoll', 'Retry')\n : t('communication_channels.profile.actions.pollNow', 'Poll now')\n const disabledReason = pushDriven\n ? t(\n 'communication_channels.profile.actions.pollNowPushDriven',\n 'This channel is push-driven \u2014 inbound messages arrive over the provider connection, so polling does not apply.',\n )\n : undefined\n const button = (\n <Button\n type=\"button\"\n variant={row.original.status === 'error' ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => void onPollNow(row.original.id)}\n disabled={!pollable}\n aria-label={disabledReason ? `${label} \u2014 ${disabledReason}` : label}\n >\n {label}\n </Button>\n )\n // A disabled button does not receive hover events in every browser, so\n // the explanation lives on a wrapper the pointer can still reach.\n return disabledReason ? <span title={disabledReason}>{button}</span> : button\n },\n },\n {\n id: 'disconnect',\n header: t('communication_channels.profile.columns.disconnect', 'Connection'),\n cell: ({ row }) => {\n const label = t('communication_channels.profile.actions.disconnect', 'Disconnect')\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setDisconnectChannel(row.original)}\n aria-label={label}\n >\n {label}\n </Button>\n )\n },\n },\n ],\n [onSetPrimary, onPollNow, onRegisterPush, t],\n )\n\n return (\n <Page>\n <PageBody>\n <header className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold\">\n {t('communication_channels.profile.title', 'My communication channels')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'communication_channels.profile.subtitle',\n 'Connect your communication channels so outbound messages come from your own account and inbound messages land in your unified inbox.',\n )}\n </p>\n </div>\n {/* Provider connect entry points injected by each channel-* package\n (channel-gmail, channel-imap) via UMES. They stack inside one\n dropdown so the header does not widen per installed provider. */}\n <ConnectChannelMenu onConnected={reloadChannels} />\n </header>\n\n {reauthRows.length > 0 ? (\n <Alert status=\"warning\" className=\"mb-4\">\n <AlertDescription>\n {t(\n 'communication_channels.profile.alerts.requiresReauth',\n '{count} channel(s) need reconnection. Click \"Reconnect\" on the affected channel below.',\n { count: reauthRows.length },\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n\n <DataTable<ChannelRow>\n title={t('communication_channels.profile.tableTitle', 'Your channels')}\n titleHeadingLevel={2}\n extensionTableId={extensionPoints.hosts.profileChannelsTable.tableId}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n error={errorMessage}\n emptyState={t(\n 'communication_channels.profile.empty',\n 'You have no connected channels yet. Add one using the menu at the top of this page.',\n )}\n />\n <ImportHistoryDialog\n channel={importChannel}\n onClose={() => setImportChannel(null)}\n onQueued={() => {\n setImportChannel(null)\n router.refresh()\n }}\n />\n <DisconnectChannelDialog\n channel={disconnectChannel}\n onClose={() => setDisconnectChannel(null)}\n onDisconnected={() => {\n setDisconnectChannel(null)\n setReloadKey((k) => k + 1)\n }}\n />\n </PageBody>\n </Page>\n )\n}\n\ntype ImportHistoryDialogProps = {\n channel: ChannelRow | null\n onClose: () => void\n onQueued: () => void\n}\n\nfunction ImportHistoryDialog({ channel, onClose, onQueued }: ImportHistoryDialogProps): React.JSX.Element {\n const t = useT()\n const importLimits = React.useMemo(() => getImportHistoryLimits(channel?.providerKey), [channel?.providerKey])\n const [sinceDays, setSinceDays] = React.useState('30')\n const [contactEmails, setContactEmails] = React.useState('')\n const [maxMessages, setMaxMessages] = React.useState('500')\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const [submitting, setSubmitting] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: IMPORT_HISTORY_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (channel) {\n setSinceDays('30')\n setContactEmails('')\n setMaxMessages('500')\n setFieldErrors({})\n setSubmitting(false)\n }\n }, [channel?.id])\n\n const handleSubmit = React.useCallback(async () => {\n if (!channel || submitting) return\n const sinceNum = Number.parseInt(sinceDays, 10)\n const maxNum = Number.parseInt(maxMessages, 10)\n const errors: Record<string, string> = {}\n if (!Number.isFinite(sinceNum) || sinceNum < 1 || sinceNum > importLimits.maxSinceDays) {\n errors.sinceDays = t(\n 'communication_channels.profile.importHistory.errors.sinceDays',\n 'Choose a number between 1 and {max} days.',\n { max: importLimits.maxSinceDays },\n )\n }\n if (!Number.isFinite(maxNum) || maxNum < 1 || maxNum > importLimits.maxMessages) {\n errors.maxMessages = t(\n 'communication_channels.profile.importHistory.errors.maxMessages',\n 'Choose a number between 1 and {max} messages.',\n { max: importLimits.maxMessages },\n )\n }\n const parsedEmails = contactEmails\n .split(/[\\s,;]+/)\n .map((s) => s.trim())\n .filter(Boolean)\n if (parsedEmails.length > 0 && parsedEmails.some((s) => !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s))) {\n errors.contactEmails = t(\n 'communication_channels.profile.importHistory.errors.contactEmails',\n 'One or more entries is not a valid email address.',\n )\n }\n if (Object.keys(errors).length > 0) {\n setFieldErrors(errors)\n return\n }\n setFieldErrors({})\n setSubmitting(true)\n const mutationPayload = {\n sinceDays: sinceNum,\n maxMessages: maxNum,\n ...(parsedEmails.length > 0 ? { contactEmails: parsedEmails } : {}),\n }\n let response\n try {\n response = await runMutation({\n operation: () => apiCall<{ progressJobId?: string }>(\n `/api/communication_channels/channels/${encodeURIComponent(channel.id)}/import-history`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(mutationPayload),\n },\n ),\n context: {\n formId: IMPORT_HISTORY_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channel.id,\n retryLastMutation,\n },\n mutationPayload,\n })\n } catch (err) {\n setSubmitting(false)\n flash(\n err instanceof Error\n ? err.message\n : t('communication_channels.profile.importHistory.flash.error', 'Failed to queue history import.'),\n 'error',\n )\n return\n }\n setSubmitting(false)\n if (!response.ok) {\n const body = response.result as { error?: string; fieldErrors?: Record<string, string> } | undefined\n if (body?.fieldErrors && Object.keys(body.fieldErrors).length > 0) {\n setFieldErrors(body.fieldErrors)\n return\n }\n flash(\n body?.error ??\n t(\n 'communication_channels.profile.importHistory.flash.error',\n 'Failed to queue history import.',\n ),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.importHistory.flash.success',\n 'History import queued \u2014 track progress in the top bar.',\n ),\n 'success',\n )\n onQueued()\n }, [channel, sinceDays, maxMessages, contactEmails, submitting, t, onQueued, retryLastMutation, runMutation, importLimits])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <Dialog open={channel !== null} onOpenChange={(open) => { if (!open) onClose() }}>\n <DialogContent onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.importHistory.title', 'Import channel history')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'communication_channels.profile.importHistory.description',\n 'Pull older messages this channel never observed at connect-time. Filters narrow the search server-side.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-since\">\n {t('communication_channels.profile.importHistory.fields.sinceDays', 'Look back (days)')}\n </Label>\n <Input\n id=\"import-history-since\"\n type=\"number\"\n min={1}\n max={importLimits.maxSinceDays}\n value={sinceDays}\n onChange={(e) => setSinceDays(e.target.value)}\n aria-invalid={Boolean(fieldErrors.sinceDays)}\n />\n {fieldErrors.sinceDays ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.sinceDays}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-emails\">\n {t(\n 'communication_channels.profile.importHistory.fields.contactEmails',\n 'Filter by sender (optional)',\n )}\n </Label>\n <Textarea\n id=\"import-history-emails\"\n rows={3}\n value={contactEmails}\n onChange={(e) => setContactEmails(e.target.value)}\n placeholder={t(\n 'communication_channels.profile.importHistory.fields.contactEmailsPlaceholder',\n 'alice@example.com, bob@example.com',\n )}\n aria-invalid={Boolean(fieldErrors.contactEmails)}\n />\n {fieldErrors.contactEmails ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.contactEmails}</p>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'communication_channels.profile.importHistory.fields.contactEmailsHint',\n 'Leave empty to scan all senders in the window.',\n )}\n </p>\n )}\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"import-history-max\">\n {t('communication_channels.profile.importHistory.fields.maxMessages', 'Maximum messages')}\n </Label>\n <Input\n id=\"import-history-max\"\n type=\"number\"\n min={1}\n max={importLimits.maxMessages}\n value={maxMessages}\n onChange={(e) => setMaxMessages(e.target.value)}\n aria-invalid={Boolean(fieldErrors.maxMessages)}\n />\n {fieldErrors.maxMessages ? (\n <p className=\"text-xs text-status-error-text\">{fieldErrors.maxMessages}</p>\n ) : null}\n </div>\n\n {fieldErrors.channelId ? (\n <Alert status=\"warning\">\n <AlertDescription>{fieldErrors.channelId}</AlertDescription>\n </Alert>\n ) : null}\n </div>\n\n <DialogFooter>\n <span className=\"mr-auto text-xs text-muted-foreground\">\n <KbdShortcut keys={['\u2318', 'Enter']} />\n </span>\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={submitting}>\n {t('communication_channels.profile.importHistory.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSubmit()} disabled={submitting}>\n {submitting\n ? t('communication_channels.profile.importHistory.submitting', 'Queueing\u2026')\n : t('communication_channels.profile.importHistory.submit', 'Start import')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\ntype DisconnectChannelDialogProps = {\n channel: ChannelRow | null\n onClose: () => void\n onDisconnected: () => void\n}\n\nfunction DisconnectChannelDialog({\n channel,\n onClose,\n onDisconnected,\n}: DisconnectChannelDialogProps): React.JSX.Element {\n const t = useT()\n const [submitting, setSubmitting] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation<ChannelMutationContext>({\n contextId: DISCONNECT_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n if (channel) setSubmitting(false)\n }, [channel?.id])\n\n const handleConfirm = React.useCallback(async () => {\n if (!channel || submitting) return\n setSubmitting(true)\n let response\n try {\n response = await runMutation({\n // optimistic-lock-exempt: self-service connect/disconnect of the\n // signed-in operator's OWN communication channel (an integration link),\n // not a shared multi-editor record. Disconnect is a terminal action\n // keyed by channel id; there is no concurrent-edit lost-update window to\n // guard, and the row carries no client-surfaced `updatedAt` round-trip.\n operation: () => apiCall(\n `/api/communication_channels/channels/${encodeURIComponent(channel.id)}`,\n { method: 'DELETE' },\n ),\n context: {\n formId: DISCONNECT_MUTATION_CONTEXT_ID,\n resourceKind: 'communication_channels.channel',\n resourceId: channel.id,\n retryLastMutation,\n },\n mutationPayload: { action: 'disconnect' },\n })\n } catch (err) {\n setSubmitting(false)\n flash(\n err instanceof Error\n ? err.message\n : t('communication_channels.profile.actions.disconnectFailed', 'Failed to disconnect channel'),\n 'error',\n )\n return\n }\n setSubmitting(false)\n if (!response.ok) {\n const body = response.result as { error?: string } | undefined\n flash(\n body?.error ??\n t('communication_channels.profile.actions.disconnectFailed', 'Failed to disconnect channel'),\n 'error',\n )\n return\n }\n flash(\n t(\n 'communication_channels.profile.actions.disconnectSuccess',\n 'Channel disconnected. You can reconnect it anytime.',\n ),\n 'success',\n )\n onDisconnected()\n }, [channel, submitting, runMutation, retryLastMutation, t, onDisconnected])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n void handleConfirm()\n }\n },\n [handleConfirm],\n )\n\n return (\n <Dialog open={channel !== null} onOpenChange={(open) => { if (!open) onClose() }}>\n <DialogContent onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.disconnect.title', 'Disconnect channel')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'communication_channels.profile.disconnect.description',\n 'This removes the connection and stops syncing. Emails already imported stay on your timelines. You can reconnect anytime.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n {channel ? (\n <p className=\"text-sm font-medium\">{channel.externalIdentifier ?? channel.displayName}</p>\n ) : null}\n\n <DialogFooter>\n <span className=\"mr-auto text-xs text-muted-foreground\">\n <KbdShortcut keys={['\u2318', 'Enter']} />\n </span>\n <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={submitting}>\n {t('communication_channels.profile.disconnect.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" variant=\"destructive-solid\" onClick={() => void handleConfirm()} disabled={submitting}>\n {submitting\n ? t('communication_channels.profile.disconnect.submitting', 'Disconnecting\u2026')\n : t('communication_channels.profile.disconnect.confirm', 'Disconnect')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction statusTag(\n status: ChannelRow['status'],\n t: (key: string, fallback?: string) => string,\n): React.ReactNode {\n switch (status) {\n case 'connected':\n return (\n <Tag variant=\"success\" dot>\n {t('communication_channels.status.connected', 'Connected')}\n </Tag>\n )\n case 'requires_reauth':\n return (\n <Tag variant=\"warning\" dot>\n {t('communication_channels.status.requiresReauth', 'Needs reconnection')}\n </Tag>\n )\n case 'error':\n return (\n <Tag variant=\"error\" dot>\n {t('communication_channels.status.error', 'Error')}\n </Tag>\n )\n case 'disconnected':\n return <Tag variant=\"neutral\">{t('communication_channels.status.disconnected', 'Disconnected')}</Tag>\n default:\n return <Tag variant=\"neutral\">{status}</Tag>\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,15 +4,31 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
|
|
|
4
4
|
import { CommunicationChannel } from "../data/entities.js";
|
|
5
5
|
import { getChannelAdapterRegistry } from "../lib/adapter-registry-singleton.js";
|
|
6
6
|
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "../lib/queue.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import {
|
|
8
|
+
IMPORT_HISTORY_MAX_CONTACT_EMAILS,
|
|
9
|
+
getImportHistoryLimits
|
|
10
|
+
} from "../lib/import-history-limits.js";
|
|
11
|
+
function createQueueImportHistorySchema(limits = getImportHistoryLimits()) {
|
|
12
|
+
return z.object({
|
|
13
|
+
channelId: z.string().uuid(),
|
|
14
|
+
sinceDays: z.number().int().min(1).max(limits.maxSinceDays).default(limits.defaultSinceDays),
|
|
15
|
+
contactEmails: z.array(z.string().email().max(255)).max(IMPORT_HISTORY_MAX_CONTACT_EMAILS).optional(),
|
|
16
|
+
maxMessages: z.number().int().min(1).max(limits.maxMessages).default(limits.defaultMaxMessages)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
let cachedSchema = null;
|
|
20
|
+
function getQueueImportHistorySchema() {
|
|
21
|
+
const limits = getImportHistoryLimits();
|
|
22
|
+
const key = `${limits.maxSinceDays}:${limits.maxMessages}`;
|
|
23
|
+
if (!cachedSchema || cachedSchema.key !== key) {
|
|
24
|
+
cachedSchema = { key, schema: createQueueImportHistorySchema(limits) };
|
|
25
|
+
}
|
|
26
|
+
return cachedSchema.schema;
|
|
27
|
+
}
|
|
28
|
+
const queueImportHistorySchema = getQueueImportHistorySchema();
|
|
13
29
|
const CHANNEL_IMPORT_HISTORY_JOB_TYPE = "communication_channels.channel.import_history";
|
|
14
30
|
async function queueImportHistory(params) {
|
|
15
|
-
const input =
|
|
31
|
+
const input = getQueueImportHistorySchema().parse(params.input);
|
|
16
32
|
const { container, scope } = params;
|
|
17
33
|
const em = container.resolve("em").fork();
|
|
18
34
|
const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId };
|
|
@@ -57,6 +73,21 @@ async function queueImportHistory(params) {
|
|
|
57
73
|
{ status: 400 }
|
|
58
74
|
);
|
|
59
75
|
}
|
|
76
|
+
const providerLimits = getImportHistoryLimits(channel.providerKey);
|
|
77
|
+
if (input.sinceDays > providerLimits.maxSinceDays) {
|
|
78
|
+
throw createCrudFormError(
|
|
79
|
+
"sinceDays exceeds the limit for this provider",
|
|
80
|
+
{ sinceDays: `Provider "${channel.providerKey}" supports at most ${providerLimits.maxSinceDays} days` },
|
|
81
|
+
{ status: 400 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (input.maxMessages > providerLimits.maxMessages) {
|
|
85
|
+
throw createCrudFormError(
|
|
86
|
+
"maxMessages exceeds the limit for this provider",
|
|
87
|
+
{ maxMessages: `Provider "${channel.providerKey}" supports at most ${providerLimits.maxMessages} messages` },
|
|
88
|
+
{ status: 400 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
60
91
|
const progressService = container.resolve("progressService");
|
|
61
92
|
const progressContext = {
|
|
62
93
|
tenantId: scope.tenantId,
|
|
@@ -107,6 +138,8 @@ async function queueImportHistory(params) {
|
|
|
107
138
|
}
|
|
108
139
|
export {
|
|
109
140
|
CHANNEL_IMPORT_HISTORY_JOB_TYPE,
|
|
141
|
+
createQueueImportHistorySchema,
|
|
142
|
+
getQueueImportHistorySchema,
|
|
110
143
|
queueImportHistory,
|
|
111
144
|
queueImportHistorySchema
|
|
112
145
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/commands/queue-import-history.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n ProgressService,\n ProgressServiceContext,\n} from '../../progress/lib/progressService'\nimport { CommunicationChannel } from '../data/entities'\nimport { getChannelAdapterRegistry } from '../lib/adapter-registry-singleton'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\n\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import.\n *\n * Validates the channel + adapter capability, creates a `ProgressJob`, then\n * enqueues a `channel-import-history` job. Returns the job id so the UI can\n * track progress via the existing ProgressTopBar.\n *\n * Concurrency guard: at most one in-flight import per channel \u2014 a second call\n * while the first is still running returns a 429 envelope via\n * `createCrudFormError`. The worker itself runs with `concurrency: 1` so a\n * dropped guard is still safe.\n */\n\nexport
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAGlB,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AAKtC,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C,SAAS,+BAA+B,qCAAqC;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n ProgressService,\n ProgressServiceContext,\n} from '../../progress/lib/progressService'\nimport { CommunicationChannel } from '../data/entities'\nimport { getChannelAdapterRegistry } from '../lib/adapter-registry-singleton'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\nimport {\n IMPORT_HISTORY_MAX_CONTACT_EMAILS,\n getImportHistoryLimits,\n type ImportHistoryLimits,\n} from '../lib/import-history-limits'\n\n/**\n * Spec B \u00A7 Phase B6 \u2014 operator-triggered backlog import.\n *\n * Validates the channel + adapter capability, creates a `ProgressJob`, then\n * enqueues a `channel-import-history` job. Returns the job id so the UI can\n * track progress via the existing ProgressTopBar.\n *\n * Concurrency guard: at most one in-flight import per channel \u2014 a second call\n * while the first is still running returns a 429 envelope via\n * `createCrudFormError`. The worker itself runs with `concurrency: 1` so a\n * dropped guard is still safe.\n */\n\nexport function createQueueImportHistorySchema(limits: ImportHistoryLimits = getImportHistoryLimits()) {\n return z.object({\n channelId: z.string().uuid(),\n sinceDays: z.number().int().min(1).max(limits.maxSinceDays).default(limits.defaultSinceDays),\n contactEmails: z\n .array(z.string().email().max(255))\n .max(IMPORT_HISTORY_MAX_CONTACT_EMAILS)\n .optional(),\n maxMessages: z.number().int().min(1).max(limits.maxMessages).default(limits.defaultMaxMessages),\n })\n}\n\nexport type QueueImportHistorySchema = ReturnType<typeof createQueueImportHistorySchema>\n\nlet cachedSchema: { key: string; schema: QueueImportHistorySchema } | null = null\n\n/**\n * The ceilings come from env, which a test may change after this module was\n * imported, so the schema is rebuilt whenever the effective limits differ from\n * the ones the cached instance was built with.\n */\nexport function getQueueImportHistorySchema(): QueueImportHistorySchema {\n const limits = getImportHistoryLimits()\n const key = `${limits.maxSinceDays}:${limits.maxMessages}`\n if (!cachedSchema || cachedSchema.key !== key) {\n cachedSchema = { key, schema: createQueueImportHistorySchema(limits) }\n }\n return cachedSchema.schema\n}\n\n/** Retained for callers that need a statically typed schema instance. */\nexport const queueImportHistorySchema = getQueueImportHistorySchema()\n\nexport type QueueImportHistoryInput = z.infer<QueueImportHistorySchema>\n\nexport interface QueueImportHistoryScope {\n tenantId: string\n organizationId: string\n userId?: string | null\n}\n\nexport interface QueueImportHistoryResult {\n progressJobId: string\n totalCountHint: number\n}\n\nexport const CHANNEL_IMPORT_HISTORY_JOB_TYPE = 'communication_channels.channel.import_history'\n\nexport interface ChannelImportHistoryJobPayload {\n progressJobId: string\n channelId: string\n sinceDays: number\n contactEmails?: string[]\n maxMessages: number\n scope: { tenantId: string; organizationId: string }\n}\n\nexport async function queueImportHistory(params: {\n container: AwilixContainer\n scope: QueueImportHistoryScope\n input: QueueImportHistoryInput\n}): Promise<QueueImportHistoryResult> {\n const input = getQueueImportHistorySchema().parse(params.input)\n const { container, scope } = params\n\n const em = (container.resolve('em') as EntityManager).fork()\n const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw createCrudFormError(\n 'Channel not found',\n { channelId: 'Channel not found' },\n { status: 404 },\n )\n }\n if (!channel.isActive || channel.status !== 'connected') {\n // 409 (not 400): the request is well-formed but the channel is in a\n // conflicting state (requires_reauth / error / disconnected). The UI maps\n // this to the localized \"reconnect first\" flash; see spec \u00A7 API Contracts.\n throw createCrudFormError(\n 'Channel is not connected',\n { channelId: 'Channel must be connected to import history' },\n { status: 409 },\n )\n }\n\n // Adapter must declare `importHistory` \u2014 Gmail / IMAP adapters gain it\n // in Spec C. Other providers (chat, SMS) cannot do historical inbox sweeps.\n const adapter = getChannelAdapterRegistry().get(channel.providerKey)\n if (!adapter) {\n throw createCrudFormError(\n 'Channel provider is not available',\n { channelId: `No adapter registered for provider \"${channel.providerKey}\"` },\n { status: 400 },\n )\n }\n if (typeof adapter.importHistory !== 'function') {\n throw createCrudFormError(\n 'History import is not supported on this provider',\n { channelId: `Provider \"${channel.providerKey}\" does not support history import yet` },\n { status: 400 },\n )\n }\n\n // The schema above validated against the deployment-wide ceiling; some\n // providers (IMAP) have a lower ceiling of their own (see\n // `import-history-limits.ts`). Reject rather than silently truncate \u2014 the\n // adapter itself has no way to report back \"I clamped your request\".\n const providerLimits = getImportHistoryLimits(channel.providerKey)\n if (input.sinceDays > providerLimits.maxSinceDays) {\n throw createCrudFormError(\n 'sinceDays exceeds the limit for this provider',\n { sinceDays: `Provider \"${channel.providerKey}\" supports at most ${providerLimits.maxSinceDays} days` },\n { status: 400 },\n )\n }\n if (input.maxMessages > providerLimits.maxMessages) {\n throw createCrudFormError(\n 'maxMessages exceeds the limit for this provider',\n { maxMessages: `Provider \"${channel.providerKey}\" supports at most ${providerLimits.maxMessages} messages` },\n { status: 400 },\n )\n }\n\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext: ProgressServiceContext = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId ?? null,\n }\n\n // Concurrency guard (best-effort): refuse if another import is already\n // in-flight for the same channel. We can't query by meta in a portable way\n // without DB-specific JSON operators, so we scan the small active-jobs window.\n // This check and `createJob` below are NOT atomic \u2014 two near-simultaneous\n // requests for the same channel can both pass and enqueue. That is acceptable,\n // not corrupting: the import worker runs at concurrency 1 (duplicate jobs run\n // sequentially, never in parallel) and inbound ingest dedups by the\n // `(channel_id, external_message_id)` unique index, so the worst case is\n // wasted re-fetch work, never duplicate rows.\n const active = await progressService.getActiveJobs(progressContext)\n const conflict = active.find((job) => {\n if (job.jobType !== CHANNEL_IMPORT_HISTORY_JOB_TYPE) return false\n const meta = (job.meta ?? {}) as Record<string, unknown>\n return meta.channelId === channel.id\n })\n if (conflict) {\n throw createCrudFormError(\n 'An import is already in progress for this channel',\n { channelId: 'Another history import is already running' },\n { status: 429 },\n )\n }\n\n const progressJob = await progressService.createJob(\n {\n jobType: CHANNEL_IMPORT_HISTORY_JOB_TYPE,\n name: `Import history: ${channel.displayName ?? channel.externalIdentifier ?? channel.providerKey}`,\n description: `Import up to ${input.maxMessages} messages from the last ${input.sinceDays} days`,\n totalCount: input.maxMessages,\n cancellable: true,\n meta: {\n channelId: channel.id,\n providerKey: channel.providerKey,\n sinceDays: input.sinceDays,\n contactEmailsCount: input.contactEmails?.length ?? 0,\n maxMessages: input.maxMessages,\n },\n },\n progressContext,\n )\n\n const payload: ChannelImportHistoryJobPayload = {\n progressJobId: progressJob.id,\n channelId: channel.id,\n sinceDays: input.sinceDays,\n contactEmails: input.contactEmails,\n maxMessages: input.maxMessages,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n }\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.importHistory)\n await queue.enqueue(payload as unknown as Record<string, unknown>)\n\n return { progressJobId: progressJob.id, totalCountHint: input.maxMessages }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAGlB,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AAKtC,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C,SAAS,+BAA+B,qCAAqC;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAeA,SAAS,+BAA+B,SAA8B,uBAAuB,GAAG;AACrG,SAAO,EAAE,OAAO;AAAA,IACd,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,IAC3B,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,OAAO,YAAY,EAAE,QAAQ,OAAO,gBAAgB;AAAA,IAC3F,eAAe,EACZ,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,CAAC,EACjC,IAAI,iCAAiC,EACrC,SAAS;AAAA,IACZ,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,OAAO,WAAW,EAAE,QAAQ,OAAO,kBAAkB;AAAA,EAChG,CAAC;AACH;AAIA,IAAI,eAAyE;AAOtE,SAAS,8BAAwD;AACtE,QAAM,SAAS,uBAAuB;AACtC,QAAM,MAAM,GAAG,OAAO,YAAY,IAAI,OAAO,WAAW;AACxD,MAAI,CAAC,gBAAgB,aAAa,QAAQ,KAAK;AAC7C,mBAAe,EAAE,KAAK,QAAQ,+BAA+B,MAAM,EAAE;AAAA,EACvE;AACA,SAAO,aAAa;AACtB;AAGO,MAAM,2BAA2B,4BAA4B;AAe7D,MAAM,kCAAkC;AAW/C,eAAsB,mBAAmB,QAIH;AACpC,QAAM,QAAQ,4BAA4B,EAAE,MAAM,OAAO,KAAK;AAC9D,QAAM,EAAE,WAAW,MAAM,IAAI;AAE7B,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM,SAAS,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAEhF,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,MAAM;AAAA,MACV,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,SAAS;AACZ,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,oBAAoB;AAAA,MACjC,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,YAAY,QAAQ,WAAW,aAAa;AAIvD,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,8CAA8C;AAAA,MAC3D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAIA,QAAM,UAAU,0BAA0B,EAAE,IAAI,QAAQ,WAAW;AACnE,MAAI,CAAC,SAAS;AACZ,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,uCAAuC,QAAQ,WAAW,IAAI;AAAA,MAC3E,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,OAAO,QAAQ,kBAAkB,YAAY;AAC/C,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,aAAa,QAAQ,WAAW,wCAAwC;AAAA,MACrF,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAMA,QAAM,iBAAiB,uBAAuB,QAAQ,WAAW;AACjE,MAAI,MAAM,YAAY,eAAe,cAAc;AACjD,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,aAAa,QAAQ,WAAW,sBAAsB,eAAe,YAAY,QAAQ;AAAA,MACtG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,MAAI,MAAM,cAAc,eAAe,aAAa;AAClD,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,aAAa,aAAa,QAAQ,WAAW,sBAAsB,eAAe,WAAW,YAAY;AAAA,MAC3G,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,kBAAkB,UAAU,QAAQ,iBAAiB;AAC3D,QAAM,kBAA0C;AAAA,IAC9C,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,IACtB,QAAQ,MAAM,UAAU;AAAA,EAC1B;AAWA,QAAM,SAAS,MAAM,gBAAgB,cAAc,eAAe;AAClE,QAAM,WAAW,OAAO,KAAK,CAAC,QAAQ;AACpC,QAAI,IAAI,YAAY,gCAAiC,QAAO;AAC5D,UAAM,OAAQ,IAAI,QAAQ,CAAC;AAC3B,WAAO,KAAK,cAAc,QAAQ;AAAA,EACpC,CAAC;AACD,MAAI,UAAU;AACZ,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,WAAW,4CAA4C;AAAA,MACzD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,gBAAgB;AAAA,IACxC;AAAA,MACE,SAAS;AAAA,MACT,MAAM,mBAAmB,QAAQ,eAAe,QAAQ,sBAAsB,QAAQ,WAAW;AAAA,MACjG,aAAa,gBAAgB,MAAM,WAAW,2BAA2B,MAAM,SAAS;AAAA,MACxF,YAAY,MAAM;AAAA,MAClB,aAAa;AAAA,MACb,MAAM;AAAA,QACJ,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,QACrB,WAAW,MAAM;AAAA,QACjB,oBAAoB,MAAM,eAAe,UAAU;AAAA,QACnD,aAAa,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,IACA;AAAA,EACF;AAEA,QAAM,UAA0C;AAAA,IAC9C,eAAe,YAAY;AAAA,IAC3B,WAAW,QAAQ;AAAA,IACnB,WAAW,MAAM;AAAA,IACjB,eAAe,MAAM;AAAA,IACrB,aAAa,MAAM;AAAA,IACnB,OAAO,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EAC1E;AACA,QAAM,QAAQ,8BAA8B,8BAA8B,aAAa;AACvF,QAAM,MAAM,QAAQ,OAA6C;AAEjE,SAAO,EAAE,eAAe,YAAY,IAAI,gBAAgB,MAAM,YAAY;AAC5E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|