@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.
@@ -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.7186.1.6e080a5017",
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.7186.1.6e080a5017",
256
- "@open-mercato/shared": "0.7.1-develop.7186.1.6e080a5017",
257
- "@open-mercato/ui": "0.7.1-develop.7186.1.6e080a5017",
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.7186.1.6e080a5017",
263
- "@open-mercato/shared": "0.7.1-develop.7186.1.6e080a5017",
264
- "@open-mercato/ui": "0.7.1-develop.7186.1.6e080a5017",
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
- // title/body are encrypted at rest (see encryption.ts). The kysely rows above
638
- // carry ciphertext when tenant encryption is enabled, so override them with the
639
- // decrypted values from findWithDecryption for the returned page.
640
- const interactionContentMap = new Map(
641
- (interactionRecords as Array<{ id: string; title?: string | null; body?: string | null }>).map(
642
- (record) => [record.id, { title: record.title ?? null, body: record.body ?? null }],
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: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.title : row.title) ?? null,
652
- body: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.body : row.body) ?? null,
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(new Date()))
91
- const [startTime, setStartTime] = React.useState('10:00')
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) : new Date()
124
- const seedDateValid = !Number.isNaN(seedDate.getTime())
125
- const fallbackNow = new Date()
126
- const dateForForm = seedDateValid ? seedDate : fallbackNow
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(new Date()))
190
- setStartTime('10:00')
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 || editData) {
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, editData, open])
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}}",