chore: expand reports coverage and upgrade next

This commit is contained in:
codex-bot 2025-10-31 17:27:51 -03:00
parent 2fb587b01d
commit 8b82284e8c
21 changed files with 2952 additions and 2713 deletions

View file

@ -1,7 +1,8 @@
// ci: trigger convex functions deploy (no-op)
import { mutation, query } from "./_generated/server"
import { api } from "./_generated/api"
import { ConvexError, v } from "convex/values"
import { paginationOptsValidator } from "convex/server"
import { ConvexError, v, Infer } from "convex/values"
import { sha256 } from "@noble/hashes/sha256"
import { randomBytes } from "@noble/hashes/utils"
import type { Doc, Id } from "./_generated/dataModel"
@ -12,6 +13,8 @@ const DEFAULT_TENANT_ID = "tenant-atlas"
const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias
const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"])
const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000
const OPEN_TICKET_STATUSES = new Set(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"])
const MACHINE_TICKETS_STATS_PAGE_SIZE = 200
type NormalizedIdentifiers = {
macs: string[]
@ -876,123 +879,128 @@ export const listByTenant = query({
},
})
export async function getByIdHandler(
ctx: QueryCtx,
args: { id: Id<"machines">; includeMetadata?: boolean }
) {
const includeMetadata = Boolean(args.includeMetadata)
const now = Date.now()
const machine = await ctx.db.get(args.id)
if (!machine) return null
const companyFromId = machine.companyId ? await ctx.db.get(machine.companyId) : null
const machineSlug = machine.companySlug ?? null
let companyFromSlug: typeof companyFromId | null = null
if (!companyFromId && machineSlug) {
companyFromSlug = await ctx.db
.query("companies")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", machineSlug))
.unique()
}
const resolvedCompany = companyFromId ?? companyFromSlug
const activeToken = await findActiveMachineToken(ctx, machine._id, now)
const offlineThresholdMs = getOfflineThresholdMs()
const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs)
const manualStatus = (machine.status ?? "").toLowerCase()
let derivedStatus: string
if (machine.isActive === false) {
derivedStatus = "deactivated"
} else if (["maintenance", "blocked"].includes(manualStatus)) {
derivedStatus = manualStatus
} else if (machine.lastHeartbeatAt) {
const age = now - machine.lastHeartbeatAt
if (age <= offlineThresholdMs) {
derivedStatus = "online"
} else if (age <= staleThresholdMs) {
derivedStatus = "offline"
} else {
derivedStatus = "stale"
}
} else {
derivedStatus = machine.status ?? "unknown"
}
const meta = includeMetadata ? (machine.metadata ?? null) : null
let metrics: Record<string, unknown> | null = null
let inventory: Record<string, unknown> | null = null
let postureAlerts: Array<Record<string, unknown>> | null = null
let lastPostureAt: number | null = null
if (meta && typeof meta === "object") {
const metaRecord = meta as Record<string, unknown>
if (metaRecord.metrics && typeof metaRecord.metrics === "object") {
metrics = metaRecord.metrics as Record<string, unknown>
}
if (metaRecord.inventory && typeof metaRecord.inventory === "object") {
inventory = metaRecord.inventory as Record<string, unknown>
}
if (Array.isArray(metaRecord.postureAlerts)) {
postureAlerts = metaRecord.postureAlerts as Array<Record<string, unknown>>
}
if (typeof metaRecord.lastPostureAt === "number") {
lastPostureAt = metaRecord.lastPostureAt as number
}
}
const linkedUserIds = machine.linkedUserIds ?? []
const linkedUsers = await Promise.all(
linkedUserIds.map(async (id) => {
const u = await ctx.db.get(id)
if (!u) return null
return { id: u._id, email: u.email, name: u.name }
})
).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>)
return {
id: machine._id,
tenantId: machine.tenantId,
hostname: machine.hostname,
companyId: machine.companyId ?? null,
companySlug: machine.companySlug ?? resolvedCompany?.slug ?? null,
companyName: resolvedCompany?.name ?? null,
osName: machine.osName,
osVersion: machine.osVersion ?? null,
architecture: machine.architecture ?? null,
macAddresses: machine.macAddresses,
serialNumbers: machine.serialNumbers,
authUserId: machine.authUserId ?? null,
authEmail: machine.authEmail ?? null,
persona: machine.persona ?? null,
assignedUserId: machine.assignedUserId ?? null,
assignedUserEmail: machine.assignedUserEmail ?? null,
assignedUserName: machine.assignedUserName ?? null,
assignedUserRole: machine.assignedUserRole ?? null,
linkedUsers,
status: derivedStatus,
isActive: machine.isActive ?? true,
lastHeartbeatAt: machine.lastHeartbeatAt ?? null,
heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.lastHeartbeatAt : null,
registeredBy: machine.registeredBy ?? null,
createdAt: machine.createdAt,
updatedAt: machine.updatedAt,
token: activeToken
? {
expiresAt: activeToken.expiresAt,
lastUsedAt: activeToken.lastUsedAt ?? null,
usageCount: activeToken.usageCount ?? 0,
}
: null,
metrics,
inventory,
postureAlerts,
lastPostureAt,
remoteAccess: machine.remoteAccess ?? null,
}
}
export const getById = query({
args: {
id: v.id("machines"),
includeMetadata: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const includeMetadata = Boolean(args.includeMetadata)
const now = Date.now()
const machine = await ctx.db.get(args.id)
if (!machine) return null
const companyFromId = machine.companyId ? await ctx.db.get(machine.companyId) : null
const machineSlug = machine.companySlug ?? null
let companyFromSlug: typeof companyFromId | null = null
if (!companyFromId && machineSlug) {
companyFromSlug = await ctx.db
.query("companies")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", machineSlug))
.unique()
}
const resolvedCompany = companyFromId ?? companyFromSlug
const activeToken = await findActiveMachineToken(ctx, machine._id, now)
const offlineThresholdMs = getOfflineThresholdMs()
const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs)
const manualStatus = (machine.status ?? "").toLowerCase()
let derivedStatus: string
if (machine.isActive === false) {
derivedStatus = "deactivated"
} else if (["maintenance", "blocked"].includes(manualStatus)) {
derivedStatus = manualStatus
} else if (machine.lastHeartbeatAt) {
const age = now - machine.lastHeartbeatAt
if (age <= offlineThresholdMs) {
derivedStatus = "online"
} else if (age <= staleThresholdMs) {
derivedStatus = "offline"
} else {
derivedStatus = "stale"
}
} else {
derivedStatus = machine.status ?? "unknown"
}
const meta = includeMetadata ? (machine.metadata ?? null) : null
let metrics: Record<string, unknown> | null = null
let inventory: Record<string, unknown> | null = null
let postureAlerts: Array<Record<string, unknown>> | null = null
let lastPostureAt: number | null = null
if (meta && typeof meta === "object") {
const metaRecord = meta as Record<string, unknown>
if (metaRecord.metrics && typeof metaRecord.metrics === "object") {
metrics = metaRecord.metrics as Record<string, unknown>
}
if (metaRecord.inventory && typeof metaRecord.inventory === "object") {
inventory = metaRecord.inventory as Record<string, unknown>
}
if (Array.isArray(metaRecord.postureAlerts)) {
postureAlerts = metaRecord.postureAlerts as Array<Record<string, unknown>>
}
if (typeof metaRecord.lastPostureAt === "number") {
lastPostureAt = metaRecord.lastPostureAt as number
}
}
const linkedUserIds = machine.linkedUserIds ?? []
const linkedUsers = await Promise.all(
linkedUserIds.map(async (id) => {
const u = await ctx.db.get(id)
if (!u) return null
return { id: u._id, email: u.email, name: u.name }
})
).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>)
return {
id: machine._id,
tenantId: machine.tenantId,
hostname: machine.hostname,
companyId: machine.companyId ?? null,
companySlug: machine.companySlug ?? resolvedCompany?.slug ?? null,
companyName: resolvedCompany?.name ?? null,
osName: machine.osName,
osVersion: machine.osVersion ?? null,
architecture: machine.architecture ?? null,
macAddresses: machine.macAddresses,
serialNumbers: machine.serialNumbers,
authUserId: machine.authUserId ?? null,
authEmail: machine.authEmail ?? null,
persona: machine.persona ?? null,
assignedUserId: machine.assignedUserId ?? null,
assignedUserEmail: machine.assignedUserEmail ?? null,
assignedUserName: machine.assignedUserName ?? null,
assignedUserRole: machine.assignedUserRole ?? null,
linkedUsers,
status: derivedStatus,
isActive: machine.isActive ?? true,
lastHeartbeatAt: machine.lastHeartbeatAt ?? null,
heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.lastHeartbeatAt : null,
registeredBy: machine.registeredBy ?? null,
createdAt: machine.createdAt,
updatedAt: machine.updatedAt,
token: activeToken
? {
expiresAt: activeToken.expiresAt,
lastUsedAt: activeToken.lastUsedAt ?? null,
usageCount: activeToken.usageCount ?? 0,
}
: null,
metrics,
inventory,
postureAlerts,
lastPostureAt,
remoteAccess: machine.remoteAccess ?? null,
}
},
handler: getByIdHandler,
})
export const listAlerts = query({
@ -1029,7 +1037,7 @@ export const listOpenTickets = query({
handler: async (ctx, { machineId, limit }) => {
const machine = await ctx.db.get(machineId)
if (!machine) {
return []
return { totalOpen: 0, hasMore: false, tickets: [] }
}
const takeLimit = Math.max(1, Math.min(limit ?? 10, 50))
const candidates = await ctx.db
@ -1041,31 +1049,392 @@ export const listOpenTickets = query({
const openTickets = candidates
.filter((ticket) => normalizeStatus(ticket.status) !== "RESOLVED")
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
.slice(0, takeLimit)
const totalOpen = openTickets.length
const limited = openTickets.slice(0, takeLimit)
return openTickets.map((ticket) => ({
id: ticket._id,
reference: ticket.reference,
subject: ticket.subject,
status: normalizeStatus(ticket.status),
priority: ticket.priority ?? "MEDIUM",
updatedAt: ticket.updatedAt,
createdAt: ticket.createdAt,
assignee: ticket.assigneeSnapshot
? {
name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null,
email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null,
}
: null,
machine: {
id: String(ticket.machineId ?? machineId),
hostname:
((ticket.machineSnapshot as { hostname?: string } | undefined)?.hostname ?? machine.hostname ?? null),
},
}))
return {
totalOpen,
hasMore: totalOpen > takeLimit,
tickets: limited.map((ticket) => ({
id: ticket._id,
reference: ticket.reference,
subject: ticket.subject,
status: normalizeStatus(ticket.status),
priority: ticket.priority ?? "MEDIUM",
updatedAt: ticket.updatedAt,
createdAt: ticket.createdAt,
assignee: ticket.assigneeSnapshot
? {
name: (ticket.assigneeSnapshot as { name?: string })?.name ?? null,
email: (ticket.assigneeSnapshot as { email?: string })?.email ?? null,
}
: null,
machine: {
id: String(ticket.machineId ?? machineId),
hostname:
((ticket.machineSnapshot as { hostname?: string } | undefined)?.hostname ?? machine.hostname ?? null),
},
})),
}
},
})
type MachineTicketsHistoryFilter = {
statusFilter: "all" | "open" | "resolved"
priorityFilter: string | null
from: number | null
to: number | null
}
type ListTicketsHistoryArgs = {
machineId: Id<"machines">
status?: "all" | "open" | "resolved"
priority?: string
search?: string
from?: number
to?: number
paginationOpts: Infer<typeof paginationOptsValidator>
}
type GetTicketsHistoryStatsArgs = {
machineId: Id<"machines">
status?: "all" | "open" | "resolved"
priority?: string
search?: string
from?: number
to?: number
}
function createMachineTicketsQuery(
ctx: QueryCtx,
machine: Doc<"machines">,
machineId: Id<"machines">,
filters: MachineTicketsHistoryFilter
) {
let working = ctx.db
.query("tickets")
.withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", machineId))
.order("desc")
if (filters.statusFilter === "open") {
working = working.filter((q) =>
q.or(
q.eq(q.field("status"), "PENDING"),
q.eq(q.field("status"), "AWAITING_ATTENDANCE"),
q.eq(q.field("status"), "PAUSED")
)
)
} else if (filters.statusFilter === "resolved") {
working = working.filter((q) => q.eq(q.field("status"), "RESOLVED"))
}
if (filters.priorityFilter) {
working = working.filter((q) => q.eq(q.field("priority"), filters.priorityFilter))
}
if (typeof filters.from === "number") {
working = working.filter((q) => q.gte(q.field("updatedAt"), filters.from!))
}
if (typeof filters.to === "number") {
working = working.filter((q) => q.lte(q.field("updatedAt"), filters.to!))
}
return working
}
function matchesTicketSearch(ticket: Doc<"tickets">, searchTerm: string): boolean {
const normalized = searchTerm.trim().toLowerCase()
if (!normalized) return true
const subject = ticket.subject.toLowerCase()
if (subject.includes(normalized)) return true
const summary = typeof ticket.summary === "string" ? ticket.summary.toLowerCase() : ""
if (summary.includes(normalized)) return true
const reference = `#${ticket.reference}`.toLowerCase()
if (reference.includes(normalized)) return true
const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
if (requesterSnapshot) {
if (requesterSnapshot.name?.toLowerCase().includes(normalized)) return true
if (requesterSnapshot.email?.toLowerCase().includes(normalized)) return true
}
const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined
if (assigneeSnapshot) {
if (assigneeSnapshot.name?.toLowerCase().includes(normalized)) return true
if (assigneeSnapshot.email?.toLowerCase().includes(normalized)) return true
}
return false
}
export async function listTicketsHistoryHandler(ctx: QueryCtx, args: ListTicketsHistoryArgs) {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
return {
page: [],
isDone: true,
continueCursor: args.paginationOpts.cursor ?? "",
}
}
const normalizedStatusFilter = args.status ?? "all"
const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null
const searchTerm = args.search?.trim().toLowerCase() ?? null
const from = typeof args.from === "number" ? args.from : null
const to = typeof args.to === "number" ? args.to : null
const filters: MachineTicketsHistoryFilter = {
statusFilter: normalizedStatusFilter,
priorityFilter: normalizedPriorityFilter,
from,
to,
}
const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate(args.paginationOpts)
const page = searchTerm ? pageResult.page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) : pageResult.page
const queueCache = new Map<string, Doc<"queues"> | null>()
const items = await Promise.all(
page.map(async (ticket) => {
let queueName: string | null = null
if (ticket.queueId) {
const key = String(ticket.queueId)
if (!queueCache.has(key)) {
queueCache.set(key, (await ctx.db.get(ticket.queueId)) as Doc<"queues"> | null)
}
queueName = queueCache.get(key)?.name ?? null
}
const requesterSnapshot = ticket.requesterSnapshot as { name?: string; email?: string } | undefined
const assigneeSnapshot = ticket.assigneeSnapshot as { name?: string; email?: string } | undefined
return {
id: ticket._id,
reference: ticket.reference,
subject: ticket.subject,
status: normalizeStatus(ticket.status),
priority: (ticket.priority ?? "MEDIUM").toString().toUpperCase(),
updatedAt: ticket.updatedAt ?? ticket.createdAt ?? 0,
createdAt: ticket.createdAt ?? 0,
queue: queueName,
requester: requesterSnapshot
? {
name: requesterSnapshot.name ?? null,
email: requesterSnapshot.email ?? null,
}
: null,
assignee: assigneeSnapshot
? {
name: assigneeSnapshot.name ?? null,
email: assigneeSnapshot.email ?? null,
}
: null,
}
})
)
return {
page: items,
isDone: pageResult.isDone,
continueCursor: pageResult.continueCursor,
splitCursor: pageResult.splitCursor ?? undefined,
pageStatus: pageResult.pageStatus ?? undefined,
}
}
export const listTicketsHistory = query({
args: {
machineId: v.id("machines"),
status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))),
priority: v.optional(v.string()),
search: v.optional(v.string()),
from: v.optional(v.number()),
to: v.optional(v.number()),
paginationOpts: paginationOptsValidator,
},
handler: listTicketsHistoryHandler,
})
export async function getTicketsHistoryStatsHandler(
ctx: QueryCtx,
args: GetTicketsHistoryStatsArgs
) {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
return { total: 0, openCount: 0, resolvedCount: 0 }
}
const normalizedStatusFilter = args.status ?? "all"
const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null
const searchTerm = args.search?.trim().toLowerCase() ?? ""
const from = typeof args.from === "number" ? args.from : null
const to = typeof args.to === "number" ? args.to : null
const filters: MachineTicketsHistoryFilter = {
statusFilter: normalizedStatusFilter,
priorityFilter: normalizedPriorityFilter,
from,
to,
}
let cursor: string | null = null
let total = 0
let openCount = 0
let done = false
while (!done) {
const pageResult = await createMachineTicketsQuery(ctx, machine, args.machineId, filters).paginate({
numItems: MACHINE_TICKETS_STATS_PAGE_SIZE,
cursor,
})
const page = searchTerm ? pageResult.page.filter((ticket) => matchesTicketSearch(ticket, searchTerm)) : pageResult.page
total += page.length
for (const ticket of page) {
if (OPEN_TICKET_STATUSES.has(normalizeStatus(ticket.status))) {
openCount += 1
}
}
done = pageResult.isDone
cursor = pageResult.continueCursor ?? null
if (!cursor) {
done = true
}
}
return {
total,
openCount,
resolvedCount: total - openCount,
}
}
export const getTicketsHistoryStats = query({
args: {
machineId: v.id("machines"),
status: v.optional(v.union(v.literal("all"), v.literal("open"), v.literal("resolved"))),
priority: v.optional(v.string()),
search: v.optional(v.string()),
from: v.optional(v.number()),
to: v.optional(v.number()),
},
handler: getTicketsHistoryStatsHandler,
})
export async function updatePersonaHandler(
ctx: MutationCtx,
args: {
machineId: Id<"machines">
persona?: string | null
assignedUserId?: Id<"users">
assignedUserEmail?: string | null
assignedUserName?: string | null
assignedUserRole?: string | null
}
) {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
throw new ConvexError("Máquina não encontrada")
}
let nextPersona = machine.persona ?? undefined
const personaProvided = args.persona !== undefined
if (args.persona !== undefined) {
const trimmed = (args.persona ?? "").trim().toLowerCase()
if (!trimmed) {
nextPersona = undefined
} else if (!ALLOWED_MACHINE_PERSONAS.has(trimmed)) {
throw new ConvexError("Perfil inválido para a máquina")
} else {
nextPersona = trimmed
}
}
let nextAssignedUserId = machine.assignedUserId ?? undefined
if (args.assignedUserId !== undefined) {
nextAssignedUserId = args.assignedUserId
}
let nextAssignedEmail = machine.assignedUserEmail ?? undefined
if (args.assignedUserEmail !== undefined) {
const trimmedEmail = (args.assignedUserEmail ?? "").trim().toLowerCase()
nextAssignedEmail = trimmedEmail || undefined
}
let nextAssignedName = machine.assignedUserName ?? undefined
if (args.assignedUserName !== undefined) {
const trimmedName = (args.assignedUserName ?? "").trim()
nextAssignedName = trimmedName || undefined
}
let nextAssignedRole = machine.assignedUserRole ?? undefined
if (args.assignedUserRole !== undefined) {
const trimmedRole = (args.assignedUserRole ?? "").trim().toUpperCase()
nextAssignedRole = trimmedRole || undefined
}
if (personaProvided && !nextPersona) {
nextAssignedUserId = undefined
nextAssignedEmail = undefined
nextAssignedName = undefined
nextAssignedRole = undefined
}
if (nextPersona && !nextAssignedUserId) {
throw new ConvexError("Associe um usuário ao definir a persona da máquina")
}
if (nextPersona && nextAssignedUserId) {
const assignedUser = await ctx.db.get(nextAssignedUserId)
if (!assignedUser) {
throw new ConvexError("Usuário vinculado não encontrado")
}
if (assignedUser.tenantId !== machine.tenantId) {
throw new ConvexError("Usuário vinculado pertence a outro tenant")
}
}
let nextMetadata = machine.metadata
if (nextPersona) {
const collaboratorMeta = {
email: nextAssignedEmail ?? null,
name: nextAssignedName ?? null,
role: nextPersona,
}
nextMetadata = mergeMetadata(machine.metadata, { collaborator: collaboratorMeta })
}
const patch: Record<string, unknown> = {
persona: nextPersona,
assignedUserId: nextPersona ? nextAssignedUserId : undefined,
assignedUserEmail: nextPersona ? nextAssignedEmail : undefined,
assignedUserName: nextPersona ? nextAssignedName : undefined,
assignedUserRole: nextPersona ? nextAssignedRole : undefined,
updatedAt: Date.now(),
}
if (nextMetadata !== machine.metadata) {
patch.metadata = nextMetadata
}
if (personaProvided) {
patch.persona = nextPersona
}
if (nextPersona) {
patch.assignedUserId = nextAssignedUserId
patch.assignedUserEmail = nextAssignedEmail
patch.assignedUserName = nextAssignedName
patch.assignedUserRole = nextAssignedRole
} else if (personaProvided) {
patch.assignedUserId = undefined
patch.assignedUserEmail = undefined
patch.assignedUserName = undefined
patch.assignedUserRole = undefined
}
await ctx.db.patch(machine._id, patch)
return { ok: true, persona: nextPersona ?? null }
}
export const updatePersona = mutation({
args: {
machineId: v.id("machines"),
@ -1075,110 +1444,7 @@ export const updatePersona = mutation({
assignedUserName: v.optional(v.string()),
assignedUserRole: v.optional(v.string()),
},
handler: async (ctx, args) => {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
throw new ConvexError("Máquina não encontrada")
}
let nextPersona = machine.persona ?? undefined
const personaProvided = args.persona !== undefined
if (args.persona !== undefined) {
const trimmed = args.persona.trim().toLowerCase()
if (!trimmed) {
nextPersona = undefined
} else if (!ALLOWED_MACHINE_PERSONAS.has(trimmed)) {
throw new ConvexError("Perfil inválido para a máquina")
} else {
nextPersona = trimmed
}
}
let nextAssignedUserId = machine.assignedUserId ?? undefined
if (args.assignedUserId !== undefined) {
nextAssignedUserId = args.assignedUserId
}
let nextAssignedEmail = machine.assignedUserEmail ?? undefined
if (args.assignedUserEmail !== undefined) {
const trimmedEmail = args.assignedUserEmail.trim().toLowerCase()
nextAssignedEmail = trimmedEmail || undefined
}
let nextAssignedName = machine.assignedUserName ?? undefined
if (args.assignedUserName !== undefined) {
const trimmedName = args.assignedUserName.trim()
nextAssignedName = trimmedName || undefined
}
let nextAssignedRole = machine.assignedUserRole ?? undefined
if (args.assignedUserRole !== undefined) {
const trimmedRole = args.assignedUserRole.trim().toUpperCase()
nextAssignedRole = trimmedRole || undefined
}
if (personaProvided && !nextPersona) {
nextAssignedUserId = undefined
nextAssignedEmail = undefined
nextAssignedName = undefined
nextAssignedRole = undefined
}
if (nextPersona && !nextAssignedUserId) {
throw new ConvexError("Associe um usuário ao definir a persona da máquina")
}
if (nextPersona && nextAssignedUserId) {
const assignedUser = await ctx.db.get(nextAssignedUserId)
if (!assignedUser) {
throw new ConvexError("Usuário vinculado não encontrado")
}
if (assignedUser.tenantId !== machine.tenantId) {
throw new ConvexError("Usuário vinculado pertence a outro tenant")
}
}
let nextMetadata = machine.metadata
if (nextPersona) {
const collaboratorMeta = {
email: nextAssignedEmail ?? null,
name: nextAssignedName ?? null,
role: nextPersona,
}
nextMetadata = mergeMetadata(machine.metadata, { collaborator: collaboratorMeta })
}
const patch: Record<string, unknown> = {
persona: nextPersona,
assignedUserId: nextPersona ? nextAssignedUserId : undefined,
assignedUserEmail: nextPersona ? nextAssignedEmail : undefined,
assignedUserName: nextPersona ? nextAssignedName : undefined,
assignedUserRole: nextPersona ? nextAssignedRole : undefined,
updatedAt: Date.now(),
}
if (nextMetadata !== machine.metadata) {
patch.metadata = nextMetadata
}
if (personaProvided) {
patch.persona = nextPersona
}
if (nextPersona) {
patch.assignedUserId = nextAssignedUserId
patch.assignedUserEmail = nextAssignedEmail
patch.assignedUserName = nextAssignedName
patch.assignedUserRole = nextAssignedRole
} else if (personaProvided) {
patch.assignedUserId = undefined
patch.assignedUserEmail = undefined
patch.assignedUserName = undefined
patch.assignedUserRole = undefined
}
await ctx.db.patch(machine._id, patch)
return { ok: true, persona: nextPersona ?? null }
},
handler: updatePersonaHandler,
})
export const getContext = query({

File diff suppressed because it is too large Load diff