Problema: Convex backend consumindo 16GB+ de RAM causando OOM kills Correcoes aplicadas: - Substituido todos os .collect() por .take(LIMIT) em 27+ arquivos - Adicionado indice by_usbPolicyStatus para otimizar query de maquinas - Corrigido N+1 problem em alerts.ts usando Map lookup - Corrigido full table scan em usbPolicy.ts - Corrigido subscription leaks no frontend (tickets-view, use-ticket-categories) - Atualizado versao do Convex backend para precompiled-2025-12-04-cc6af4c Arquivos principais modificados: - convex/*.ts - limites em todas as queries .collect() - convex/schema.ts - novo indice by_usbPolicyStatus - convex/alerts.ts - N+1 fix com Map - convex/usbPolicy.ts - uso do novo indice - src/components/tickets/tickets-view.tsx - skip condicional - src/hooks/use-ticket-categories.ts - skip condicional 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
import { mutation, query } from "./_generated/server"
|
|
import { ConvexError, v } from "convex/values"
|
|
|
|
import { requireAdmin } from "./rbac"
|
|
|
|
const PRIORITY_VALUES = ["URGENT", "HIGH", "MEDIUM", "LOW", "DEFAULT"] as const
|
|
const VALID_STATUSES = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED", "RESOLVED"] as const
|
|
const VALID_TIME_MODES = ["business", "calendar"] as const
|
|
|
|
type CategorySlaRuleInput = {
|
|
priority: string
|
|
responseTargetMinutes?: number | null
|
|
responseMode?: string | null
|
|
solutionTargetMinutes?: number | null
|
|
solutionMode?: string | null
|
|
alertThreshold?: number | null
|
|
pauseStatuses?: string[] | null
|
|
calendarType?: string | null
|
|
}
|
|
|
|
const ruleInput = v.object({
|
|
priority: v.string(),
|
|
responseTargetMinutes: v.optional(v.number()),
|
|
responseMode: v.optional(v.string()),
|
|
solutionTargetMinutes: v.optional(v.number()),
|
|
solutionMode: v.optional(v.string()),
|
|
alertThreshold: v.optional(v.number()),
|
|
pauseStatuses: v.optional(v.array(v.string())),
|
|
calendarType: v.optional(v.string()),
|
|
})
|
|
|
|
function normalizePriority(value: string) {
|
|
const upper = value.trim().toUpperCase()
|
|
return PRIORITY_VALUES.includes(upper as (typeof PRIORITY_VALUES)[number]) ? upper : "DEFAULT"
|
|
}
|
|
|
|
function sanitizeTime(value?: number | null) {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined
|
|
return Math.round(value)
|
|
}
|
|
|
|
function normalizeMode(value?: string | null) {
|
|
if (!value) return "calendar"
|
|
const normalized = value.toLowerCase()
|
|
return VALID_TIME_MODES.includes(normalized as (typeof VALID_TIME_MODES)[number]) ? normalized : "calendar"
|
|
}
|
|
|
|
function normalizeThreshold(value?: number | null) {
|
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
return 0.8
|
|
}
|
|
const clamped = Math.min(Math.max(value, 0.1), 0.95)
|
|
return Math.round(clamped * 100) / 100
|
|
}
|
|
|
|
function normalizePauseStatuses(value?: string[] | null) {
|
|
if (!Array.isArray(value)) return ["PAUSED"]
|
|
const normalized = new Set<string>()
|
|
for (const status of value) {
|
|
if (typeof status !== "string") continue
|
|
const upper = status.trim().toUpperCase()
|
|
if (VALID_STATUSES.includes(upper as (typeof VALID_STATUSES)[number])) {
|
|
normalized.add(upper)
|
|
}
|
|
}
|
|
if (normalized.size === 0) {
|
|
normalized.add("PAUSED")
|
|
}
|
|
return Array.from(normalized)
|
|
}
|
|
|
|
export const get = query({
|
|
args: {
|
|
tenantId: v.string(),
|
|
viewerId: v.id("users"),
|
|
categoryId: v.id("ticketCategories"),
|
|
},
|
|
handler: async (ctx, { tenantId, viewerId, categoryId }) => {
|
|
await requireAdmin(ctx, viewerId, tenantId)
|
|
const category = await ctx.db.get(categoryId)
|
|
if (!category || category.tenantId !== tenantId) {
|
|
throw new ConvexError("Categoria não encontrada")
|
|
}
|
|
const records = await ctx.db
|
|
.query("categorySlaSettings")
|
|
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
|
|
.take(100)
|
|
|
|
return {
|
|
categoryId,
|
|
categoryName: category.name,
|
|
rules: records.map((record) => ({
|
|
priority: record.priority,
|
|
responseTargetMinutes: record.responseTargetMinutes ?? null,
|
|
responseMode: record.responseMode ?? "calendar",
|
|
solutionTargetMinutes: record.solutionTargetMinutes ?? null,
|
|
solutionMode: record.solutionMode ?? "calendar",
|
|
alertThreshold: record.alertThreshold ?? 0.8,
|
|
pauseStatuses: record.pauseStatuses ?? ["PAUSED"],
|
|
})),
|
|
}
|
|
},
|
|
})
|
|
|
|
export const save = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
categoryId: v.id("ticketCategories"),
|
|
rules: v.array(ruleInput),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, categoryId, rules }) => {
|
|
await requireAdmin(ctx, actorId, tenantId)
|
|
const category = await ctx.db.get(categoryId)
|
|
if (!category || category.tenantId !== tenantId) {
|
|
throw new ConvexError("Categoria não encontrada")
|
|
}
|
|
const sanitized = sanitizeRules(rules)
|
|
const existing = await ctx.db
|
|
.query("categorySlaSettings")
|
|
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
|
|
.take(100)
|
|
await Promise.all(existing.map((record) => ctx.db.delete(record._id)))
|
|
|
|
const now = Date.now()
|
|
for (const rule of sanitized) {
|
|
await ctx.db.insert("categorySlaSettings", {
|
|
tenantId,
|
|
categoryId,
|
|
priority: rule.priority,
|
|
responseTargetMinutes: rule.responseTargetMinutes,
|
|
responseMode: rule.responseMode,
|
|
solutionTargetMinutes: rule.solutionTargetMinutes,
|
|
solutionMode: rule.solutionMode,
|
|
alertThreshold: rule.alertThreshold,
|
|
pauseStatuses: rule.pauseStatuses,
|
|
calendarType: rule.calendarType ?? undefined,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
actorId,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
|
|
function sanitizeRules(rules: CategorySlaRuleInput[]) {
|
|
const normalized: Record<string, ReturnType<typeof buildRule>> = {}
|
|
for (const rule of rules) {
|
|
const built = buildRule(rule)
|
|
normalized[built.priority] = built
|
|
}
|
|
return Object.values(normalized)
|
|
}
|
|
|
|
function buildRule(rule: CategorySlaRuleInput) {
|
|
const priority = normalizePriority(rule.priority)
|
|
const responseTargetMinutes = sanitizeTime(rule.responseTargetMinutes)
|
|
const solutionTargetMinutes = sanitizeTime(rule.solutionTargetMinutes)
|
|
return {
|
|
priority,
|
|
responseTargetMinutes,
|
|
responseMode: normalizeMode(rule.responseMode),
|
|
solutionTargetMinutes,
|
|
solutionMode: normalizeMode(rule.solutionMode),
|
|
alertThreshold: normalizeThreshold(rule.alertThreshold),
|
|
pauseStatuses: normalizePauseStatuses(rule.pauseStatuses),
|
|
calendarType: rule.calendarType ?? null,
|
|
}
|
|
}
|