feat: melhorar gerenciamento de acesso remoto de máquinas

This commit is contained in:
Esdras Renan 2025-10-28 11:45:16 -03:00
parent 714b199879
commit 192a5c2909
5 changed files with 664 additions and 261 deletions

View file

@ -18,6 +18,10 @@ export const ensureDefaults = mutation({
await ctx.db.patch(queue._id, { name: "Laboratório", slug: "laboratorio" });
return (await ctx.db.get(queue._id)) ?? queue;
}
if (queue.name === "Field Services" || queue.slug === "field-services") {
await ctx.db.patch(queue._id, { name: "Visitas", slug: "visitas" });
return (await ctx.db.get(queue._id)) ?? queue;
}
return queue;
})
);
@ -25,7 +29,7 @@ export const ensureDefaults = mutation({
const queues = [
{ name: "Chamados", slug: "chamados" },
{ name: "Laboratório", slug: "laboratorio" },
{ name: "Field Services", slug: "field-services" },
{ name: "Visitas", slug: "visitas" },
];
for (const q of queues) {
await ctx.db.insert("queues", { tenantId, name: q.name, slug: q.slug, teamId: undefined });
@ -33,4 +37,3 @@ export const ensureDefaults = mutation({
}
},
});

View file

@ -1367,6 +1367,118 @@ export const toggleActive = mutation({
},
})
type RemoteAccessEntry = {
id: string
provider: string
identifier: string
url: string | null
notes: string | null
lastVerifiedAt: number | null
metadata: Record<string, unknown> | null
}
function createRemoteAccessId() {
return `ra_${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36)}`
}
function coerceString(value: unknown): string | null {
if (typeof value === "string") {
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : null
}
return null
}
function coerceNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const trimmed = value.trim()
if (!trimmed) return null
const parsed = Number(trimmed)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null {
if (!raw) return null
if (typeof raw === "string") {
const trimmed = raw.trim()
if (!trimmed) return null
const isUrl = /^https?:\/\//i.test(trimmed)
return {
id: createRemoteAccessId(),
provider: "Remoto",
identifier: isUrl ? trimmed : trimmed,
url: isUrl ? trimmed : null,
notes: null,
lastVerifiedAt: null,
metadata: null,
}
}
if (typeof raw !== "object") return null
const record = raw as Record<string, unknown>
const provider =
coerceString(record.provider) ??
coerceString(record.tool) ??
coerceString(record.vendor) ??
coerceString(record.name) ??
"Remoto"
const identifier =
coerceString(record.identifier) ??
coerceString(record.code) ??
coerceString(record.id) ??
coerceString(record.accessId)
const url =
coerceString(record.url) ??
coerceString(record.link) ??
coerceString(record.remoteUrl) ??
coerceString(record.console) ??
coerceString(record.viewer) ??
null
const resolvedIdentifier = identifier ?? url ?? "Acesso remoto"
const notes = coerceString(record.notes) ?? coerceString(record.note) ?? coerceString(record.description) ?? coerceString(record.obs) ?? null
const timestamp =
coerceNumber(record.lastVerifiedAt) ??
coerceNumber(record.verifiedAt) ??
coerceNumber(record.checkedAt) ??
coerceNumber(record.updatedAt) ??
null
const id = coerceString(record.id) ?? createRemoteAccessId()
const metadata =
record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata)
? (record.metadata as Record<string, unknown>)
: null
return {
id,
provider,
identifier: resolvedIdentifier,
url,
notes,
lastVerifiedAt: timestamp,
metadata,
}
}
function normalizeRemoteAccessList(raw: unknown): RemoteAccessEntry[] {
const source = Array.isArray(raw) ? raw : raw ? [raw] : []
const seen = new Set<string>()
const entries: RemoteAccessEntry[] = []
for (const item of source) {
const entry = normalizeRemoteAccessEntry(item)
if (!entry) continue
let nextId = entry.id
while (seen.has(nextId)) {
nextId = createRemoteAccessId()
}
seen.add(nextId)
entries.push(nextId === entry.id ? entry : { ...entry, id: nextId })
}
return entries
}
export const updateRemoteAccess = mutation({
args: {
machineId: v.id("machines"),
@ -1375,9 +1487,11 @@ export const updateRemoteAccess = mutation({
identifier: v.optional(v.string()),
url: v.optional(v.string()),
notes: v.optional(v.string()),
action: v.optional(v.string()),
entryId: v.optional(v.string()),
clear: v.optional(v.boolean()),
},
handler: async (ctx, { machineId, actorId, provider, identifier, url, notes, clear }) => {
handler: async (ctx, { machineId, actorId, provider, identifier, url, notes, action, entryId, clear }) => {
const machine = await ctx.db.get(machineId)
if (!machine) {
throw new ConvexError("Máquina não encontrada")
@ -1393,11 +1507,49 @@ export const updateRemoteAccess = mutation({
throw new ConvexError("Somente administradores e agentes podem ajustar o acesso remoto.")
}
if (clear) {
const actionMode = (() => {
if (clear) return "clear" as const
const normalized = (action ?? "").toLowerCase()
if (normalized === "clear") return "clear" as const
if (normalized === "delete" || normalized === "remove") return "delete" as const
return "upsert" as const
})()
const existingEntries = normalizeRemoteAccessList(machine.remoteAccess)
if (actionMode === "clear") {
await ctx.db.patch(machineId, { remoteAccess: null, updatedAt: Date.now() })
return { remoteAccess: null }
}
if (actionMode === "delete") {
const trimmedEntryId = coerceString(entryId)
const trimmedProvider = coerceString(provider)
const trimmedIdentifier = coerceString(identifier)
let target: RemoteAccessEntry | undefined
if (trimmedEntryId) {
target = existingEntries.find((entry) => entry.id === trimmedEntryId)
}
if (!target && trimmedProvider && trimmedIdentifier) {
target = existingEntries.find(
(entry) => entry.provider === trimmedProvider && entry.identifier === trimmedIdentifier
)
}
if (!target && trimmedIdentifier) {
target = existingEntries.find((entry) => entry.identifier === trimmedIdentifier)
}
if (!target && trimmedProvider) {
target = existingEntries.find((entry) => entry.provider === trimmedProvider)
}
if (!target) {
throw new ConvexError("Entrada de acesso remoto não encontrada.")
}
const nextEntries = existingEntries.filter((entry) => entry.id !== target!.id)
const nextValue = nextEntries.length > 0 ? nextEntries : null
await ctx.db.patch(machineId, { remoteAccess: nextValue, updatedAt: Date.now() })
return { remoteAccess: nextValue }
}
const trimmedProvider = (provider ?? "").trim()
const trimmedIdentifier = (identifier ?? "").trim()
if (!trimmedProvider || !trimmedIdentifier) {
@ -1422,8 +1574,15 @@ export const updateRemoteAccess = mutation({
const cleanedNotes = notes?.trim() ? notes.trim() : null
const lastVerifiedAt = Date.now()
const targetEntryId =
coerceString(entryId) ??
existingEntries.find(
(entry) => entry.provider === trimmedProvider && entry.identifier === trimmedIdentifier
)?.id ??
createRemoteAccessId()
const remoteAccess = {
const updatedEntry: RemoteAccessEntry = {
id: targetEntryId,
provider: trimmedProvider,
identifier: trimmedIdentifier,
url: normalizedUrl,
@ -1438,12 +1597,21 @@ export const updateRemoteAccess = mutation({
},
}
const existingIndex = existingEntries.findIndex((entry) => entry.id === targetEntryId)
let nextEntries: RemoteAccessEntry[]
if (existingIndex >= 0) {
nextEntries = [...existingEntries]
nextEntries[existingIndex] = updatedEntry
} else {
nextEntries = [...existingEntries, updatedEntry]
}
await ctx.db.patch(machineId, {
remoteAccess,
remoteAccess: nextEntries,
updatedAt: Date.now(),
})
return { remoteAccess }
return { remoteAccess: nextEntries }
},
})