@open-mercato/core 0.7.1-develop.7186.1.6e080a5017 → 0.7.1-develop.7193.1.910a5b0a1e
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/customers/api/interactions/decryptedFields.js +31 -0
- package/dist/modules/customers/api/interactions/decryptedFields.js.map +7 -0
- package/dist/modules/customers/api/interactions/route.js +23 -10
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +15 -2
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +35 -10
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/query_index/api/status.js +19 -12
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/interactions/decryptedFields.ts +52 -0
- package/src/modules/customers/api/interactions/route.ts +40 -12
- package/src/modules/customers/components/detail/TasksSection.tsx +15 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +58 -10
- package/src/modules/customers/i18n/de.json +3 -0
- package/src/modules/customers/i18n/en.json +3 -0
- package/src/modules/customers/i18n/es.json +3 -0
- package/src/modules/customers/i18n/ko.json +3 -0
- package/src/modules/customers/i18n/pl.json +3 -0
- package/src/modules/query_index/api/status.ts +27 -15
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/api/status.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport {\n envDisablesAutoIndexing,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n} from '@open-mercato/shared/lib/search/auto-indexing'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n // Resolve search module configs to determine which entities each search backend covers.\n // Entities with buildSource defined are vector-search capable; entities with a fieldPolicy\n // are fulltext-capable.\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorConfiguredEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n const searchConfiguredEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorConfiguredEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n }\n }\n }\n\n let searchStrategies: unknown[] = []\n try {\n searchStrategies = (container.resolve('searchStrategies') as unknown[]) ?? []\n } catch {\n searchStrategies = []\n }\n\n // Resolve fulltext strategy for entity counts\n const fulltextStrategy = (searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n\n // Vector coverage is only a meaningful signal when embeddings can actually be written:\n // the instance has not switched auto-indexing off, an embedding provider is reachable,\n // and the tenant has not opted out. Reporting a permanent `0 / n` gap on installs with\n // no embedding provider trains operators to ignore this page.\n const vectorRuntimeEnabled = await (async () => {\n if (vectorConfiguredEntities.size === 0) return false\n if (envDisablesAutoIndexing()) return false\n const vectorStrategy = searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'vector',\n ) as { isAvailable?: () => Promise<boolean> } | undefined\n if (typeof vectorStrategy?.isAvailable !== 'function') return false\n try {\n if (!(await vectorStrategy.isAvailable())) return false\n } catch {\n return false\n }\n try {\n const moduleConfigService = container.resolve('moduleConfigService') as ModuleConfigService\n const value = await moduleConfigService.getValue<boolean>(\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n { defaultValue: true, scope: { tenantId } },\n )\n return value !== false\n } catch {\n return true\n }\n })()\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Entities with active custom field definitions in the current scope. This is reported\n // per row so the client can filter on it \u2014 it is NOT a gate on which entities are listed.\n const customFieldEntities = new Set<string>()\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n for (const row of cfRows || []) customFieldEntities.add(String(row.entity_id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n const idleJobSummary = () => ({ status: 'idle' as const, partitions: [] as any[] })\n\n // Job rows for every listed entity are fetched in a single query. This endpoint is polled\n // every few seconds and the entity list is no longer capped at custom-field entities, so a\n // per-entity round trip here would scale the poll cost with the number of indexed entities.\n async function fetchJobSummaries(\n entityTypes: string[],\n tenantIdParam: string | null,\n organizationIdParam: string | null,\n ): Promise<Map<string, ReturnType<typeof buildJobSummary>>> {\n const byEntity = new Map<string, ReturnType<typeof buildJobSummary>>()\n if (!entityTypes.length) return byEntity\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, 'in', entityTypes)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n const rowsByEntity = new Map<string, Array<Record<string, any>>>()\n for (const row of rows) {\n const entityType = String(row.entity_type ?? '')\n if (!entityType) continue\n const bucket = rowsByEntity.get(entityType)\n if (bucket) bucket.push(row)\n else rowsByEntity.set(entityType, [row])\n }\n for (const [entityType, entityRows] of rowsByEntity) {\n byEntity.set(entityType, buildJobSummary(entityRows, organizationIdParam, tenantIdParam))\n }\n } catch {\n return byEntity\n }\n return byEntity\n }\n\n function buildJobSummary(\n rows: Array<Record<string, any>>,\n organizationIdParam: string | null,\n tenantIdParam: string | null,\n ) {\n if (!rows.length) {\n return idleJobSummary()\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? (row.status === 'failed' ? 'failed' : 'completed')\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n } else if (\n partitions.some((p) => p.status === 'failed')\n || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')\n ) {\n // The run finished but lost records; without this it reports \"idle\" and the only\n // hint that anything went wrong is the coverage percentage.\n status = 'failed'\n }\n\n const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null\n const finishedAt = status === 'idle' || status === 'failed'\n ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null)\n : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: (() => {\n const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null\n const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null\n if (finishedDate) return scopeCandidate!.row.status === 'failed' ? 'failed' : 'completed'\n if (\n !heartbeatDate ||\n Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS\n ) {\n return 'stalled'\n }\n return (scopeCandidate!.row.status as string) || 'reindexing'\n })(),\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: generatedIds, ...coverageScope })\n\n const hasIndexCoverage = (entityId: string): boolean => {\n const snapshot = snapshotByEntity.get(entityId)\n if (!snapshot) return false\n return snapshot.baseCount > 0 || snapshot.indexedCount > 0 || snapshot.vectorIndexedCount > 0\n }\n\n // Nothing this page reports is custom-field dependent: `lib/indexer.ts` builds the index\n // doc from the base row and only attaches `cf:*`/`l10n:*` keys when they exist, and\n // `applyEntityIndexesJoin` runs on every query regardless of custom fields. Gating the\n // list on `custom_field_defs` hid every fully-indexed entity that happens to have no\n // custom fields, so its coverage could neither be inspected nor reindexed from here.\n // List whatever any backend actually covers and let the client filter.\n const entityIds = generatedIds.filter(\n (id) => searchConfiguredEntities.has(id) || customFieldEntities.has(id) || hasIndexCoverage(id),\n )\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobsByEntity = await fetchJobSummaries(entityIds, tenantId, organizationId)\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobsByEntity.get(eid) ?? idleJobSummary()\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n // `vectorEnabled` and `vectorCount` keep their published meaning \u2014 \"the entity declares\n // buildSource\" and its raw coverage \u2014 so existing consumers see no change. Whether vector\n // indexing can actually run ships additively as `vectorIndexingActive`.\n const vectorEnabled = vectorConfiguredEntities.has(eid)\n const vectorCountNumber = vectorEnabled\n ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count)\n : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n\n // `ok` keeps its published aggregate meaning (query index AND configured vector coverage)\n // so consumers using it as a health signal are unaffected. The narrower signal this page\n // needs \u2014 is the query index in sync with the base table \u2014 ships additively as\n // `queryIndexOk`. Folding vector into the badge is what made every vector-capable entity\n // read \"Out of sync\" while base == indexed.\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n const queryIndexOk = baseCountNumber != null\n && indexCountNumber != null\n && baseCountNumber === indexCountNumber\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorCountNumber,\n vectorEnabled,\n vectorIndexingActive: vectorEnabled && vectorRuntimeEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n hasCustomFields: customFieldEntities.has(eid),\n ok,\n queryIndexOk,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport {\n envDisablesAutoIndexing,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n} from '@open-mercato/shared/lib/search/auto-indexing'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n // Resolve search module configs to determine which entities each search backend covers.\n // Entities with buildSource defined are vector-search capable; entities with a fieldPolicy\n // are fulltext-capable.\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorConfiguredEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n const searchConfiguredEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorConfiguredEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n }\n }\n }\n\n let searchStrategies: unknown[] = []\n try {\n searchStrategies = (container.resolve('searchStrategies') as unknown[]) ?? []\n } catch {\n searchStrategies = []\n }\n\n // Resolve fulltext strategy for entity counts\n const fulltextStrategy = (searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n\n // Vector coverage is only a meaningful signal when embeddings can actually be written:\n // the instance has not switched auto-indexing off, an embedding provider is reachable,\n // and the tenant has not opted out. Reporting a permanent `0 / n` gap on installs with\n // no embedding provider trains operators to ignore this page.\n const vectorRuntimeEnabled = await (async () => {\n if (vectorConfiguredEntities.size === 0) return false\n if (envDisablesAutoIndexing()) return false\n const vectorStrategy = searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'vector',\n ) as { isAvailable?: () => Promise<boolean> } | undefined\n if (typeof vectorStrategy?.isAvailable !== 'function') return false\n try {\n if (!(await vectorStrategy.isAvailable())) return false\n } catch {\n return false\n }\n try {\n const moduleConfigService = container.resolve('moduleConfigService') as ModuleConfigService\n const value = await moduleConfigService.getValue<boolean>(\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n { defaultValue: true, scope: { tenantId } },\n )\n return value !== false\n } catch {\n return true\n }\n })()\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Entities with active custom field definitions in the current scope. This is reported\n // per row so the client can filter on it \u2014 it is NOT a gate on which entities are listed.\n const customFieldEntities = new Set<string>()\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n for (const row of cfRows || []) customFieldEntities.add(String(row.entity_id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n const idleJobSummary = () => ({ status: 'idle' as const, partitions: [] as any[] })\n\n // Job rows for every listed entity are fetched in a single query. This endpoint is polled\n // every few seconds and the entity list is no longer capped at custom-field entities, so a\n // per-entity round trip here would scale the poll cost with the number of indexed entities.\n async function fetchJobSummaries(\n entityTypes: string[],\n tenantIdParam: string | null,\n organizationIdParam: string | null,\n ): Promise<Map<string, ReturnType<typeof buildJobSummary>>> {\n const byEntity = new Map<string, ReturnType<typeof buildJobSummary>>()\n if (!entityTypes.length) return byEntity\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, 'in', entityTypes)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n const rowsByEntity = new Map<string, Array<Record<string, any>>>()\n for (const row of rows) {\n const entityType = String(row.entity_type ?? '')\n if (!entityType) continue\n const bucket = rowsByEntity.get(entityType)\n if (bucket) bucket.push(row)\n else rowsByEntity.set(entityType, [row])\n }\n for (const [entityType, entityRows] of rowsByEntity) {\n byEntity.set(entityType, buildJobSummary(entityRows, organizationIdParam, tenantIdParam))\n }\n } catch {\n return byEntity\n }\n return byEntity\n }\n\n function buildJobSummary(\n rows: Array<Record<string, any>>,\n organizationIdParam: string | null,\n tenantIdParam: string | null,\n ) {\n if (!rows.length) {\n return idleJobSummary()\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? (row.status === 'failed' ? 'failed' : 'completed')\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n // A scope-only job (no partition rows, e.g. partitionCount: 1) has nothing in\n // `partitions` for the checks below to see, so its running/stalled/failed state and\n // timestamps must fall back to the scope row itself instead of defaulting to idle/null.\n const computeScopeStatus = (row: any): 'failed' | 'completed' | 'stalled' | 'reindexing' | 'purging' => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n if (finishedDate) return row.status === 'failed' ? 'failed' : 'completed'\n if (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS) {\n return 'stalled'\n }\n return (row.status as 'reindexing' | 'purging' | undefined | null) || 'reindexing'\n }\n const scopeStatus = scopeCandidate ? computeScopeStatus(scopeCandidate.row) : null\n const scopeStartedAt = scopeCandidate?.row.started_at ? new Date(scopeCandidate.row.started_at).toISOString() : null\n const scopeFinishedAt = scopeCandidate?.row.finished_at ? new Date(scopeCandidate.row.finished_at).toISOString() : null\n const scopeHeartbeatAt = scopeCandidate?.row.heartbeat_at ? new Date(scopeCandidate.row.heartbeat_at).toISOString() : null\n\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n } else if (\n partitions.some((p) => p.status === 'failed')\n || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')\n ) {\n // The run finished but lost records; without this it reports \"idle\" and the only\n // hint that anything went wrong is the coverage percentage.\n status = 'failed'\n } else if (!partitions.length && scopeStatus && scopeStatus !== 'completed') {\n status = scopeStatus\n }\n\n const startedAt = activePartitions[0]?.startedAt\n ?? partitions[0]?.startedAt\n ?? (!partitions.length ? scopeStartedAt : null)\n const finishedAt = status === 'idle' || status === 'failed'\n ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? (!partitions.length ? scopeFinishedAt : null))\n : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt\n ?? partitions[0]?.heartbeatAt\n ?? (!partitions.length ? scopeHeartbeatAt : null)\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: scopeStatus!,\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: generatedIds, ...coverageScope })\n\n const hasIndexCoverage = (entityId: string): boolean => {\n const snapshot = snapshotByEntity.get(entityId)\n if (!snapshot) return false\n return snapshot.baseCount > 0 || snapshot.indexedCount > 0 || snapshot.vectorIndexedCount > 0\n }\n\n // Nothing this page reports is custom-field dependent: `lib/indexer.ts` builds the index\n // doc from the base row and only attaches `cf:*`/`l10n:*` keys when they exist, and\n // `applyEntityIndexesJoin` runs on every query regardless of custom fields. Gating the\n // list on `custom_field_defs` hid every fully-indexed entity that happens to have no\n // custom fields, so its coverage could neither be inspected nor reindexed from here.\n // List whatever any backend actually covers and let the client filter.\n const entityIds = generatedIds.filter(\n (id) => searchConfiguredEntities.has(id) || customFieldEntities.has(id) || hasIndexCoverage(id),\n )\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobsByEntity = await fetchJobSummaries(entityIds, tenantId, organizationId)\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobsByEntity.get(eid) ?? idleJobSummary()\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n // `vectorEnabled` and `vectorCount` keep their published meaning \u2014 \"the entity declares\n // buildSource\" and its raw coverage \u2014 so existing consumers see no change. Whether vector\n // indexing can actually run ships additively as `vectorIndexingActive`.\n const vectorEnabled = vectorConfiguredEntities.has(eid)\n const vectorCountNumber = vectorEnabled\n ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count)\n : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n\n // `ok` keeps its published aggregate meaning (query index AND configured vector coverage)\n // so consumers using it as a health signal are unaffected. The narrower signal this page\n // needs \u2014 is the query index in sync with the base table \u2014 ships additively as\n // `queryIndexOk`. Folding vector into the badge is what made every vector-capable entity\n // read \"Out of sync\" while base == indexed.\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n const queryIndexOk = baseCountNumber != null\n && indexCountNumber != null\n && baseCountNumber === indexCountNumber\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorCountNumber,\n vectorEnabled,\n vectorIndexingActive: vectorEnabled && vectorRuntimeEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n hasCustomFields: customFieldEntities.has(eid),\n ok,\n queryIndexOk,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7193.1.910a5b0a1e",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.1",
|
|
267
267
|
"@testing-library/react": "^16.3.3",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { fieldNameCandidates } from '@open-mercato/shared/lib/query/encrypted-sort'
|
|
2
|
+
|
|
3
|
+
function normalizeDecryptedValue(value: unknown): unknown {
|
|
4
|
+
if (value === undefined || value === null) return null
|
|
5
|
+
if (value instanceof Date) return value.toISOString()
|
|
6
|
+
return value
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findKey(target: Record<string, unknown>, field: string): string | null {
|
|
10
|
+
for (const candidate of fieldNameCandidates(field)) {
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(target, candidate)) return candidate
|
|
12
|
+
}
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Overrides the response fields an entity's encryption map covers with the
|
|
18
|
+
* plaintext carried by its decrypted ORM record.
|
|
19
|
+
*
|
|
20
|
+
* List routes that read their base rows through Kysely get raw column values,
|
|
21
|
+
* so every field the map declares arrives as ciphertext. Encryption maps are
|
|
22
|
+
* configurable per deployment, so the covered set is resolved at runtime rather
|
|
23
|
+
* than hard-coded — extending a map must not leave a field passing through as
|
|
24
|
+
* ciphertext (#5945).
|
|
25
|
+
*
|
|
26
|
+
* Map fields are authored as column names (`recurrence_rule`) while response
|
|
27
|
+
* keys and entity properties are camelCase (`recurrenceRule`), so each declared
|
|
28
|
+
* field is matched through the shared candidate spellings. Fields that resolve
|
|
29
|
+
* to neither a response key nor a record property are skipped, which keeps the
|
|
30
|
+
* response shape unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export function applyDecryptedFields<T extends Record<string, unknown>>(
|
|
33
|
+
item: T,
|
|
34
|
+
decryptedRecord: object | undefined,
|
|
35
|
+
encryptedFields: readonly string[],
|
|
36
|
+
): T {
|
|
37
|
+
if (!decryptedRecord || encryptedFields.length === 0) return item
|
|
38
|
+
// ORM entities are class instances rather than index-signature types; the
|
|
39
|
+
// encryption map addresses their columns by name, so read them as a record.
|
|
40
|
+
const source = decryptedRecord as Record<string, unknown>
|
|
41
|
+
let patched: T | null = null
|
|
42
|
+
for (const field of encryptedFields) {
|
|
43
|
+
const responseKey = findKey(item, field)
|
|
44
|
+
const recordKey = findKey(source, field)
|
|
45
|
+
if (!responseKey || !recordKey) continue
|
|
46
|
+
const value = normalizeDecryptedValue(source[recordKey])
|
|
47
|
+
if (value === item[responseKey]) continue
|
|
48
|
+
patched = patched ?? { ...item }
|
|
49
|
+
;(patched as Record<string, unknown>)[responseKey] = value
|
|
50
|
+
}
|
|
51
|
+
return patched ?? item
|
|
52
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { CUSTOMER_INTERACTION_ENTITY_ID } from '../../lib/interactionCompatibility'
|
|
28
28
|
import { applyEmailVisibilityFilter } from '../../lib/visibilityFilter'
|
|
29
29
|
import { resolveEncryptedSortPage } from './encryptedSortPage'
|
|
30
|
+
import { applyDecryptedFields } from './decryptedFields'
|
|
30
31
|
import { resolveCanonicalActivityTargetId } from '../../lib/legacyActivityBridge'
|
|
31
32
|
import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
32
33
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
@@ -603,8 +604,22 @@ export async function GET(req: Request) {
|
|
|
603
604
|
),
|
|
604
605
|
)
|
|
605
606
|
const interactionIds = pageRows.map((row) => row.id)
|
|
607
|
+
// A page can span the selected organization plus its descendants
|
|
608
|
+
// (organizationScope.ts expands a concrete selection that way), and
|
|
609
|
+
// encryption maps are resolved per organization with first-match-wins —
|
|
610
|
+
// an org-specific map replaces rather than merges with the tenant-wide
|
|
611
|
+
// one. Resolving a single field set from `selectedOrganizationId` and
|
|
612
|
+
// applying it to every row therefore missed descendant-org fields the
|
|
613
|
+
// row's own map covers (#5945 follow-up). Each row's own
|
|
614
|
+
// `organization_id` is resolved instead, one lookup per distinct
|
|
615
|
+
// organization on the page — `getEncryptedFieldNames` memoizes per
|
|
616
|
+
// (entity, tenant, organization), so this adds no query for the common
|
|
617
|
+
// single-organization case.
|
|
618
|
+
const pageOrganizationIds = Array.from(
|
|
619
|
+
new Set(pageRows.map((row) => row.organization_id).filter((value): value is string => !!value)),
|
|
620
|
+
)
|
|
606
621
|
|
|
607
|
-
const [users, deals, customFieldValues, interactionRecords] = await Promise.all([
|
|
622
|
+
const [users, deals, customFieldValues, interactionRecords, encryptedFieldsByOrganization] = await Promise.all([
|
|
608
623
|
authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),
|
|
609
624
|
dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),
|
|
610
625
|
interactionIds.length > 0
|
|
@@ -620,6 +635,17 @@ export async function GET(req: Request) {
|
|
|
620
635
|
interactionIds.length > 0
|
|
621
636
|
? findWithDecryption(em, CustomerInteraction, { id: { $in: interactionIds } } as never, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId })
|
|
622
637
|
: Promise.resolve([]),
|
|
638
|
+
(async () => {
|
|
639
|
+
const byOrganization = new Map<string, readonly string[]>()
|
|
640
|
+
if (interactionIds.length === 0 || !encryptionService?.getEncryptedFieldNames || pageOrganizationIds.length === 0) {
|
|
641
|
+
return byOrganization
|
|
642
|
+
}
|
|
643
|
+
await Promise.all(pageOrganizationIds.map(async (organizationId) => {
|
|
644
|
+
const fields = await encryptionService.getEncryptedFieldNames(CUSTOMER_INTERACTION_ENTITY_ID, auth.tenantId, organizationId)
|
|
645
|
+
byOrganization.set(organizationId, fields)
|
|
646
|
+
}))
|
|
647
|
+
return byOrganization
|
|
648
|
+
})(),
|
|
623
649
|
])
|
|
624
650
|
|
|
625
651
|
const userMap = new Map(
|
|
@@ -634,22 +660,24 @@ export async function GET(req: Request) {
|
|
|
634
660
|
const dealMap = new Map(
|
|
635
661
|
deals.map((deal) => [deal.id, deal.title]),
|
|
636
662
|
)
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
663
|
+
// The kysely rows above carry raw column values, so every field the entity's
|
|
664
|
+
// encryption map covers arrives as ciphertext. findWithDecryption already
|
|
665
|
+
// returned those fields in plaintext, so the response takes them from the
|
|
666
|
+
// decrypted records. The covered set is read from each row's own
|
|
667
|
+
// organization's resolved map rather than hard-coded or shared across the
|
|
668
|
+
// page, so extending the map — or a page spanning several organizations —
|
|
669
|
+
// cannot leave a field passing through as ciphertext (#5945).
|
|
670
|
+
const interactionRecordMap = new Map<string, CustomerInteraction>(
|
|
671
|
+
(interactionRecords as CustomerInteraction[]).map((record) => [record.id, record]),
|
|
644
672
|
)
|
|
645
673
|
|
|
646
|
-
const baseItems = pageRows.map((row) => ({
|
|
674
|
+
const baseItems = pageRows.map((row) => applyDecryptedFields({
|
|
647
675
|
id: row.id,
|
|
648
676
|
entityId: row.entity_id,
|
|
649
677
|
dealId: row.deal_id ?? null,
|
|
650
678
|
interactionType: row.interaction_type,
|
|
651
|
-
title:
|
|
652
|
-
body:
|
|
679
|
+
title: row.title ?? null,
|
|
680
|
+
body: row.body ?? null,
|
|
653
681
|
status: row.status,
|
|
654
682
|
scheduledAt: toIsoString(row.scheduled_at),
|
|
655
683
|
occurredAt: toIsoString(row.occurred_at),
|
|
@@ -680,7 +708,7 @@ export async function GET(req: Request) {
|
|
|
680
708
|
authorEmail: row.author_user_id ? userMap.get(row.author_user_id)?.email ?? null : null,
|
|
681
709
|
dealTitle: row.deal_id ? dealMap.get(row.deal_id) ?? null : null,
|
|
682
710
|
customValues: normalizeCustomFieldResponse(customFieldValues[row.id]) ?? null,
|
|
683
|
-
}))
|
|
711
|
+
}, interactionRecordMap.get(row.id), encryptedFieldsByOrganization.get(row.organization_id) ?? []))
|
|
684
712
|
|
|
685
713
|
const enricherContext = await buildEnricherContext(
|
|
686
714
|
container,
|
|
@@ -9,6 +9,7 @@ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitive
|
|
|
9
9
|
import { mapDictionaryColorToTone } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
10
10
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
11
11
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
12
13
|
import { LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
|
|
13
14
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
14
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -115,6 +116,7 @@ export function TasksSection({
|
|
|
115
116
|
const tHook = useT()
|
|
116
117
|
const fallbackTranslator = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])
|
|
117
118
|
const t: Translator = React.useMemo(() => translator ?? fallbackTranslator, [translator, fallbackTranslator])
|
|
119
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
118
120
|
const runWriteMutation = React.useCallback(
|
|
119
121
|
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
120
122
|
if (!runGuardedMutation) {
|
|
@@ -354,6 +356,16 @@ export function TasksSection({
|
|
|
354
356
|
|
|
355
357
|
const handleDelete = React.useCallback(
|
|
356
358
|
async (task: TodoLinkSummary) => {
|
|
359
|
+
const approved = await confirm({
|
|
360
|
+
title: t('customers.people.detail.tasks.deleteConfirmTitle', 'Remove task?'),
|
|
361
|
+
description: t(
|
|
362
|
+
'customers.people.detail.tasks.deleteConfirmDescription',
|
|
363
|
+
'This task will be removed from every view it appears in. This action cannot be undone.',
|
|
364
|
+
),
|
|
365
|
+
confirmText: t('customers.people.detail.tasks.deleteConfirmAction', 'Remove task'),
|
|
366
|
+
variant: 'destructive',
|
|
367
|
+
})
|
|
368
|
+
if (!approved) return
|
|
357
369
|
try {
|
|
358
370
|
await runWriteMutation(
|
|
359
371
|
() => unlinkTask(task),
|
|
@@ -371,7 +383,7 @@ export function TasksSection({
|
|
|
371
383
|
flash(message, 'error')
|
|
372
384
|
}
|
|
373
385
|
},
|
|
374
|
-
[onDataRefresh, refresh, runWriteMutation, t, unlinkTask],
|
|
386
|
+
[confirm, onDataRefresh, refresh, runWriteMutation, t, unlinkTask],
|
|
375
387
|
)
|
|
376
388
|
|
|
377
389
|
const handleCancel = React.useCallback(
|
|
@@ -620,6 +632,8 @@ export function TasksSection({
|
|
|
620
632
|
contextMessage={dialogContextMessage}
|
|
621
633
|
useCanonicalInteractions={useCanonicalInteractions}
|
|
622
634
|
/>
|
|
635
|
+
|
|
636
|
+
{ConfirmDialogElement}
|
|
623
637
|
</div>
|
|
624
638
|
)
|
|
625
639
|
}
|
|
@@ -67,10 +67,49 @@ const DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {
|
|
|
67
67
|
note: 15,
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Create-mode date/time defaults. A fixed morning slot made every activity opened
|
|
71
|
+
// later in the day start in the past, so the seed is always computed forward from
|
|
72
|
+
// "now" (#5940): tasks are plan-ahead artefacts and default to the end of the
|
|
73
|
+
// working day, everything else to the next half-hour slot.
|
|
74
|
+
const DEFAULT_SLOT_MINUTES = 30
|
|
75
|
+
const TASK_DEFAULT_HOUR = 17
|
|
76
|
+
const NEXT_DAY_START_HOUR = 9
|
|
77
|
+
|
|
70
78
|
function padDatePart(value: number): string {
|
|
71
79
|
return String(value).padStart(2, '0')
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
function isSameLocalDay(left: Date, right: Date): boolean {
|
|
83
|
+
return (
|
|
84
|
+
left.getFullYear() === right.getFullYear() &&
|
|
85
|
+
left.getMonth() === right.getMonth() &&
|
|
86
|
+
left.getDate() === right.getDate()
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function nextSlotAfter(now: Date): Date {
|
|
91
|
+
const next = new Date(now)
|
|
92
|
+
next.setSeconds(0, 0)
|
|
93
|
+
next.setMinutes(next.getMinutes() + (DEFAULT_SLOT_MINUTES - (next.getMinutes() % DEFAULT_SLOT_MINUTES)))
|
|
94
|
+
if (!isSameLocalDay(next, now)) {
|
|
95
|
+
next.setHours(NEXT_DAY_START_HOUR, 0, 0, 0)
|
|
96
|
+
}
|
|
97
|
+
return next
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default start moment for a newly created activity of `type`, always strictly
|
|
102
|
+
* after `now` so a brand-new record is never born overdue (#5940).
|
|
103
|
+
*/
|
|
104
|
+
export function resolveDefaultActivityStart(type: ActivityType, now: Date): Date {
|
|
105
|
+
if (type === 'task') {
|
|
106
|
+
const endOfWorkingDay = new Date(now)
|
|
107
|
+
endOfWorkingDay.setHours(TASK_DEFAULT_HOUR, 0, 0, 0)
|
|
108
|
+
if (endOfWorkingDay.getTime() > now.getTime()) return endOfWorkingDay
|
|
109
|
+
}
|
|
110
|
+
return nextSlotAfter(now)
|
|
111
|
+
}
|
|
112
|
+
|
|
74
113
|
function formatLocalDateInput(date: Date): string {
|
|
75
114
|
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
|
76
115
|
}
|
|
@@ -85,10 +124,15 @@ interface UseScheduleFormStateParams {
|
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
export function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {
|
|
127
|
+
// An empty `id` is the menu-driven "New X" convention — a create with a preset
|
|
128
|
+
// type, not an edit. The save path already reads it this way (`isSaveEdit`), so
|
|
129
|
+
// the seeding below must agree or new records inherit edit-mode fallbacks (#5940).
|
|
130
|
+
const isEditing = Boolean(editData?.id)
|
|
131
|
+
const [initialStart] = React.useState(() => resolveDefaultActivityStart('meeting', new Date()))
|
|
88
132
|
const [activityType, setActivityType] = React.useState<ActivityType>('meeting')
|
|
89
133
|
const [title, setTitle] = React.useState('')
|
|
90
|
-
const [date, setDate] = React.useState(() => formatLocalDateInput(
|
|
91
|
-
const [startTime, setStartTime] = React.useState(
|
|
134
|
+
const [date, setDate] = React.useState(() => formatLocalDateInput(initialStart))
|
|
135
|
+
const [startTime, setStartTime] = React.useState(() => formatLocalTimeInput(initialStart))
|
|
92
136
|
const [duration, setDuration] = React.useState(30)
|
|
93
137
|
const [allDay, setAllDay] = React.useState(false)
|
|
94
138
|
const [description, setDescription] = React.useState('')
|
|
@@ -120,10 +164,13 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
120
164
|
// Keep seed values in the user's local timezone, matching the cluster-E
|
|
121
165
|
// local-day convention.
|
|
122
166
|
const sourceTimestamp = editData.occurredAt ?? editData.scheduledAt ?? null
|
|
123
|
-
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) :
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const dateForForm =
|
|
167
|
+
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : null
|
|
168
|
+
// No usable timestamp means this is a preset create (or a corrupt row), so
|
|
169
|
+
// fall forward to the create-mode default instead of "now" (#5940).
|
|
170
|
+
const dateForForm =
|
|
171
|
+
seedDate && !Number.isNaN(seedDate.getTime())
|
|
172
|
+
? seedDate
|
|
173
|
+
: resolveDefaultActivityStart(resolvedType, new Date())
|
|
127
174
|
setDate(formatLocalDateInput(dateForForm))
|
|
128
175
|
setStartTime(formatLocalTimeInput(dateForForm))
|
|
129
176
|
setDuration(editData.durationMinutes ?? 30)
|
|
@@ -184,10 +231,11 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
184
231
|
}
|
|
185
232
|
} else {
|
|
186
233
|
// Create mode: reset all fields
|
|
234
|
+
const defaultStart = resolveDefaultActivityStart('meeting', new Date())
|
|
187
235
|
setActivityType('meeting')
|
|
188
236
|
setTitle('')
|
|
189
|
-
setDate(formatLocalDateInput(
|
|
190
|
-
setStartTime(
|
|
237
|
+
setDate(formatLocalDateInput(defaultStart))
|
|
238
|
+
setStartTime(formatLocalTimeInput(defaultStart))
|
|
191
239
|
setDuration(30)
|
|
192
240
|
setAllDay(false)
|
|
193
241
|
setDescription('')
|
|
@@ -212,14 +260,14 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
212
260
|
// avoid flipping the default in a closed-but-mounted dialog.
|
|
213
261
|
const lastReminderTypeRef = React.useRef<ActivityType>('meeting')
|
|
214
262
|
React.useEffect(() => {
|
|
215
|
-
if (!open ||
|
|
263
|
+
if (!open || isEditing) {
|
|
216
264
|
lastReminderTypeRef.current = activityType
|
|
217
265
|
return
|
|
218
266
|
}
|
|
219
267
|
if (lastReminderTypeRef.current === activityType) return
|
|
220
268
|
lastReminderTypeRef.current = activityType
|
|
221
269
|
setReminderMinutes(DEFAULT_REMINDER_MINUTES[activityType])
|
|
222
|
-
}, [activityType,
|
|
270
|
+
}, [activityType, isEditing, open])
|
|
223
271
|
|
|
224
272
|
const removeParticipant = React.useCallback((index: number) => {
|
|
225
273
|
setParticipants((prev) => prev.filter((_, i) => i !== index))
|
|
@@ -2201,6 +2201,9 @@
|
|
|
2201
2201
|
"customers.people.detail.tasks.cancelSuccess": "Aufgabe abgebrochen",
|
|
2202
2202
|
"customers.people.detail.tasks.completeSuccess": "Aufgabe als erledigt markiert",
|
|
2203
2203
|
"customers.people.detail.tasks.createSuccess": "Aufgabe erstellt",
|
|
2204
|
+
"customers.people.detail.tasks.deleteConfirmAction": "Aufgabe entfernen",
|
|
2205
|
+
"customers.people.detail.tasks.deleteConfirmDescription": "Die Aufgabe wird aus allen Ansichten entfernt, in denen sie erscheint. Diese Aktion kann nicht rückgängig gemacht werden.",
|
|
2206
|
+
"customers.people.detail.tasks.deleteConfirmTitle": "Aufgabe entfernen?",
|
|
2204
2207
|
"customers.people.detail.tasks.deleteError": "Aufgabe konnte nicht entfernt werden",
|
|
2205
2208
|
"customers.people.detail.tasks.deleteSuccess": "Aufgabe entfernt",
|
|
2206
2209
|
"customers.people.detail.tasks.dialog.context": "Diese Aufgabe wird mit {{name}} verknüpft",
|
|
@@ -2201,6 +2201,9 @@
|
|
|
2201
2201
|
"customers.people.detail.tasks.cancelSuccess": "Task canceled",
|
|
2202
2202
|
"customers.people.detail.tasks.completeSuccess": "Task marked as done",
|
|
2203
2203
|
"customers.people.detail.tasks.createSuccess": "Task created",
|
|
2204
|
+
"customers.people.detail.tasks.deleteConfirmAction": "Remove task",
|
|
2205
|
+
"customers.people.detail.tasks.deleteConfirmDescription": "This task will be removed from every view it appears in. This action cannot be undone.",
|
|
2206
|
+
"customers.people.detail.tasks.deleteConfirmTitle": "Remove task?",
|
|
2204
2207
|
"customers.people.detail.tasks.deleteError": "Failed to remove task",
|
|
2205
2208
|
"customers.people.detail.tasks.deleteSuccess": "Task removed",
|
|
2206
2209
|
"customers.people.detail.tasks.dialog.context": "This task will be linked to {{name}}",
|
|
@@ -2201,6 +2201,9 @@
|
|
|
2201
2201
|
"customers.people.detail.tasks.cancelSuccess": "Tarea cancelada",
|
|
2202
2202
|
"customers.people.detail.tasks.completeSuccess": "Tarea marcada como completada",
|
|
2203
2203
|
"customers.people.detail.tasks.createSuccess": "Tarea creada",
|
|
2204
|
+
"customers.people.detail.tasks.deleteConfirmAction": "Eliminar tarea",
|
|
2205
|
+
"customers.people.detail.tasks.deleteConfirmDescription": "La tarea se eliminará de todas las vistas en las que aparece. Esta acción no se puede deshacer.",
|
|
2206
|
+
"customers.people.detail.tasks.deleteConfirmTitle": "¿Eliminar tarea?",
|
|
2204
2207
|
"customers.people.detail.tasks.deleteError": "No se pudo eliminar la tarea",
|
|
2205
2208
|
"customers.people.detail.tasks.deleteSuccess": "Tarea eliminada",
|
|
2206
2209
|
"customers.people.detail.tasks.dialog.context": "Esta tarea se vinculará con {{name}}",
|
|
@@ -2201,6 +2201,9 @@
|
|
|
2201
2201
|
"customers.people.detail.tasks.cancelSuccess": "작업이 취소되었습니다",
|
|
2202
2202
|
"customers.people.detail.tasks.completeSuccess": "작업이 완료로 표시되었습니다",
|
|
2203
2203
|
"customers.people.detail.tasks.createSuccess": "작업이 생성되었습니다",
|
|
2204
|
+
"customers.people.detail.tasks.deleteConfirmAction": "작업 제거",
|
|
2205
|
+
"customers.people.detail.tasks.deleteConfirmDescription": "이 작업은 표시되는 모든 화면에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.",
|
|
2206
|
+
"customers.people.detail.tasks.deleteConfirmTitle": "작업을 제거하시겠습니까?",
|
|
2204
2207
|
"customers.people.detail.tasks.deleteError": "작업 제거에 실패했습니다",
|
|
2205
2208
|
"customers.people.detail.tasks.deleteSuccess": "작업이 제거되었습니다",
|
|
2206
2209
|
"customers.people.detail.tasks.dialog.context": "이 작업은 {{name}}에 연결됩니다",
|
|
@@ -2201,6 +2201,9 @@
|
|
|
2201
2201
|
"customers.people.detail.tasks.cancelSuccess": "Zadanie anulowano",
|
|
2202
2202
|
"customers.people.detail.tasks.completeSuccess": "Zadanie oznaczone jako wykonane",
|
|
2203
2203
|
"customers.people.detail.tasks.createSuccess": "Zadanie utworzone",
|
|
2204
|
+
"customers.people.detail.tasks.deleteConfirmAction": "Usuń zadanie",
|
|
2205
|
+
"customers.people.detail.tasks.deleteConfirmDescription": "Zadanie zostanie usunięte ze wszystkich widoków, w których się pojawia. Tej operacji nie można cofnąć.",
|
|
2206
|
+
"customers.people.detail.tasks.deleteConfirmTitle": "Usunąć zadanie?",
|
|
2204
2207
|
"customers.people.detail.tasks.deleteError": "Nie udało się usunąć zadania",
|
|
2205
2208
|
"customers.people.detail.tasks.deleteSuccess": "Zadanie usunięte",
|
|
2206
2209
|
"customers.people.detail.tasks.dialog.context": "To zadanie będzie powiązane z {{name}}",
|