fix(convex): corrigir memory leak com .collect() sem limite e adicionar otimizacoes

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>
This commit is contained in:
esdrasrenan 2025-12-09 21:30:06 -03:00
parent a4b46b08ba
commit 638faeb287
33 changed files with 139 additions and 128 deletions

View file

@ -25,6 +25,7 @@ import type * as fields from "../fields.js";
import type * as files from "../files.js"; import type * as files from "../files.js";
import type * as incidents from "../incidents.js"; import type * as incidents from "../incidents.js";
import type * as invites from "../invites.js"; import type * as invites from "../invites.js";
import type * as liveChat from "../liveChat.js";
import type * as machines from "../machines.js"; import type * as machines from "../machines.js";
import type * as metrics from "../metrics.js"; import type * as metrics from "../metrics.js";
import type * as migrations from "../migrations.js"; import type * as migrations from "../migrations.js";
@ -74,6 +75,7 @@ declare const fullApi: ApiFromModules<{
files: typeof files; files: typeof files;
incidents: typeof incidents; incidents: typeof incidents;
invites: typeof invites; invites: typeof invites;
liveChat: typeof liveChat;
machines: typeof machines; machines: typeof machines;
metrics: typeof metrics; metrics: typeof metrics;
migrations: typeof migrations; migrations: typeof migrations;

View file

@ -46,7 +46,7 @@ export const list = query({
let items = await ctx.db let items = await ctx.db
.query("alerts") .query("alerts")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
if (companyId) items = items.filter((a) => a.companyId === companyId) if (companyId) items = items.filter((a) => a.companyId === companyId)
if (typeof start === "number") items = items.filter((a) => a.createdAt >= start) if (typeof start === "number") items = items.filter((a) => a.createdAt >= start)
if (typeof end === "number") items = items.filter((a) => a.createdAt < end) if (typeof end === "number") items = items.filter((a) => a.createdAt < end)
@ -62,7 +62,7 @@ export const managersForCompany = query({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
.collect() .take(100)
return users.filter((u) => (u.role ?? "").toUpperCase() === "MANAGER") return users.filter((u) => (u.role ?? "").toUpperCase() === "MANAGER")
}, },
}) })
@ -78,7 +78,7 @@ export const lastForCompanyBySlug = query({
const items = await ctx.db const items = await ctx.db
.query("alerts") .query("alerts")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
const matches = items.filter((a) => a.companyId === company._id) const matches = items.filter((a) => a.companyId === company._id)
if (matches.length === 0) return null if (matches.length === 0) return null
const last = matches.sort((a, b) => b.createdAt - a.createdAt)[0] const last = matches.sort((a, b) => b.createdAt - a.createdAt)[0]
@ -94,12 +94,15 @@ export const lastForCompaniesBySlugs = query({
const alerts = await ctx.db const alerts = await ctx.db
.query("alerts") .query("alerts")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
// Buscar todas as companies do tenant de uma vez
const allCompanies = await ctx.db
.query("companies")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(1000)
const companiesBySlug = new Map(allCompanies.map(c => [c.slug, c]))
for (const slug of slugs) { for (const slug of slugs) {
const company = await ctx.db const company = companiesBySlug.get(slug)
.query("companies")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug))
.first()
if (!company) { if (!company) {
result[slug] = null result[slug] = null
continue continue

View file

@ -7,7 +7,7 @@ export const ensureDefaults = mutation({
let existing = await ctx.db let existing = await ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(10);
existing = await Promise.all( existing = await Promise.all(
existing.map(async (queue) => { existing.map(async (queue) => {
if (queue.name === "Suporte N1" || queue.slug === "suporte-n1") { if (queue.name === "Suporte N1" || queue.slug === "suporte-n1") {

View file

@ -208,7 +208,7 @@ export const list = query({
const categories = await ctx.db const categories = await ctx.db
.query("ticketCategories") .query("ticketCategories")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
if (categories.length === 0) { if (categories.length === 0) {
return [] return []
@ -217,7 +217,7 @@ export const list = query({
const subcategories = await ctx.db const subcategories = await ctx.db
.query("ticketSubcategories") .query("ticketSubcategories")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return categories.map((category) => ({ return categories.map((category) => ({
id: category._id, id: category._id,
@ -249,7 +249,7 @@ export const ensureDefaults = mutation({
const existingCount = await ctx.db const existingCount = await ctx.db
.query("ticketCategories") .query("ticketCategories")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
if (existingCount.length > 0) { if (existingCount.length > 0) {
return { created: 0 } return { created: 0 }
@ -408,7 +408,7 @@ export const deleteCategory = mutation({
const subs = await ctx.db const subs = await ctx.db
.query("ticketSubcategories") .query("ticketSubcategories")
.withIndex("by_category_order", (q) => q.eq("categoryId", categoryId)) .withIndex("by_category_order", (q) => q.eq("categoryId", categoryId))
.collect() .take(100)
for (const sub of subs) { for (const sub of subs) {
await ctx.db.patch(sub._id, { await ctx.db.patch(sub._id, {
categoryId: transferTo, categoryId: transferTo,
@ -418,7 +418,7 @@ export const deleteCategory = mutation({
const ticketsToMove = await ctx.db const ticketsToMove = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId)) .withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
.collect() .take(100)
for (const ticket of ticketsToMove) { for (const ticket of ticketsToMove) {
await ctx.db.patch(ticket._id, { await ctx.db.patch(ticket._id, {
categoryId: transferTo, categoryId: transferTo,
@ -437,7 +437,7 @@ export const deleteCategory = mutation({
const subs = await ctx.db const subs = await ctx.db
.query("ticketSubcategories") .query("ticketSubcategories")
.withIndex("by_category_order", (q) => q.eq("categoryId", categoryId)) .withIndex("by_category_order", (q) => q.eq("categoryId", categoryId))
.collect() .take(100)
for (const sub of subs) { for (const sub of subs) {
await ctx.db.delete(sub._id) await ctx.db.delete(sub._id)
} }
@ -530,7 +530,7 @@ export const deleteSubcategory = mutation({
const tickets = await ctx.db const tickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_subcategory", (q) => q.eq("tenantId", tenantId).eq("subcategoryId", subcategoryId)) .withIndex("by_tenant_subcategory", (q) => q.eq("tenantId", tenantId).eq("subcategoryId", subcategoryId))
.collect() .take(100)
for (const ticket of tickets) { for (const ticket of tickets) {
await ctx.db.patch(ticket._id, { await ctx.db.patch(ticket._id, {
subcategoryId: transferTo, subcategoryId: transferTo,

View file

@ -84,7 +84,7 @@ export const get = query({
const records = await ctx.db const records = await ctx.db
.query("categorySlaSettings") .query("categorySlaSettings")
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId)) .withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
.collect() .take(100)
return { return {
categoryId, categoryId,
@ -119,7 +119,7 @@ export const save = mutation({
const existing = await ctx.db const existing = await ctx.db
.query("categorySlaSettings") .query("categorySlaSettings")
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId)) .withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
.collect() .take(100)
await Promise.all(existing.map((record) => ctx.db.delete(record._id))) await Promise.all(existing.map((record) => ctx.db.delete(record._id)))
const now = Date.now() const now = Date.now()

View file

@ -58,7 +58,7 @@ export const list = query({
const templates = await ctx.db const templates = await ctx.db
.query("commentTemplates") .query("commentTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return templates return templates
.filter((template) => (template.kind ?? "comment") === normalizedKind) .filter((template) => (template.kind ?? "comment") === normalizedKind)

View file

@ -23,7 +23,7 @@ export const list = query({
const companies = await ctx.db const companies = await ctx.db
.query("companies") .query("companies")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(200)
return companies.map((c) => ({ id: c._id, name: c.name, slug: c.slug })) return companies.map((c) => ({ id: c._id, name: c.name, slug: c.slug }))
}, },
}) })
@ -131,7 +131,7 @@ export const removeBySlug = mutation({
const relatedTickets = await ctx.db const relatedTickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", existing._id)) .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", existing._id))
.collect() .take(200)
if (relatedTickets.length > 0) { if (relatedTickets.length > 0) {
const companySnapshot = { const companySnapshot = {
name: existing.name, name: existing.name,

View file

@ -219,7 +219,7 @@ export const list = query({
const dashboards = await ctx.db const dashboards = await ctx.db
.query("dashboards") .query("dashboards")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
const filtered = (includeArchived ? dashboards : dashboards.filter((d) => !(d.isArchived ?? false))).sort( const filtered = (includeArchived ? dashboards : dashboards.filter((d) => !(d.isArchived ?? false))).sort(
(a, b) => b.updatedAt - a.updatedAt, (a, b) => b.updatedAt - a.updatedAt,
@ -230,7 +230,7 @@ export const list = query({
const widgets = await ctx.db const widgets = await ctx.db
.query("dashboardWidgets") .query("dashboardWidgets")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboard._id)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboard._id))
.collect() .take(100)
return { return {
...sanitizeDashboard(dashboard), ...sanitizeDashboard(dashboard),
widgetsCount: widgets.length, widgetsCount: widgets.length,
@ -256,14 +256,14 @@ export const get = query({
const widgets = await ctx.db const widgets = await ctx.db
.query("dashboardWidgets") .query("dashboardWidgets")
.withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(100)
widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt) widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)
const shares = await ctx.db const shares = await ctx.db
.query("dashboardShares") .query("dashboardShares")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(50)
return { return {
dashboard: sanitizeDashboard(dashboard), dashboard: sanitizeDashboard(dashboard),
@ -457,7 +457,7 @@ export const updateLayout = mutation({
const widgets = await ctx.db const widgets = await ctx.db
.query("dashboardWidgets") .query("dashboardWidgets")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(100)
const byKey = new Map<string, Doc<"dashboardWidgets">>() const byKey = new Map<string, Doc<"dashboardWidgets">>()
widgets.forEach((widget) => byKey.set(widget.widgetKey, widget)) widgets.forEach((widget) => byKey.set(widget.widgetKey, widget))
@ -518,7 +518,7 @@ export const addWidget = mutation({
const existingWidgets = await ctx.db const existingWidgets = await ctx.db
.query("dashboardWidgets") .query("dashboardWidgets")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(100)
const widgetId = await ctx.db.insert("dashboardWidgets", { const widgetId = await ctx.db.insert("dashboardWidgets", {
tenantId, tenantId,
@ -617,7 +617,7 @@ export const ensureQueueSummaryWidget = mutation({
const widgets = await ctx.db const widgets = await ctx.db
.query("dashboardWidgets") .query("dashboardWidgets")
.withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(100)
widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt) widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)
@ -871,7 +871,7 @@ export const upsertShare = mutation({
const existingShares = await ctx.db const existingShares = await ctx.db
.query("dashboardShares") .query("dashboardShares")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(50)
const now = Date.now() const now = Date.now()
let shareDoc = existingShares.find((share) => share.audience === audience) let shareDoc = existingShares.find((share) => share.audience === audience)
@ -917,7 +917,7 @@ export const revokeShareToken = mutation({
const shares = await ctx.db const shares = await ctx.db
.query("dashboardShares") .query("dashboardShares")
.withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId)) .withIndex("by_dashboard", (q) => q.eq("dashboardId", dashboardId))
.collect() .take(50)
for (const share of shares) { for (const share of shares) {
if (share.audience === "public-link") { if (share.audience === "public-link") {

View file

@ -37,7 +37,7 @@ async function unsetDefaults(
const templates = await ctx.db const templates = await ctx.db
.query("deviceExportTemplates") .query("deviceExportTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
await Promise.all( await Promise.all(
templates templates
@ -73,7 +73,7 @@ export const list = query({
const templates = await ctx.db const templates = await ctx.db
.query("deviceExportTemplates") .query("deviceExportTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return templates return templates
.filter((tpl) => { .filter((tpl) => {
@ -112,7 +112,7 @@ export const listForTenant = query({
const templates = await ctx.db const templates = await ctx.db
.query("deviceExportTemplates") .query("deviceExportTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return templates return templates
.filter((tpl) => tpl.isActive !== false) .filter((tpl) => tpl.isActive !== false)
@ -149,7 +149,7 @@ export const getDefault = query({
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
: ctx.db.query("deviceExportTemplates").withIndex("by_tenant_default", (q) => q.eq("tenantId", tenantId).eq("isDefault", true)) : ctx.db.query("deviceExportTemplates").withIndex("by_tenant_default", (q) => q.eq("tenantId", tenantId).eq("isDefault", true))
const templates = await indexQuery.collect() const templates = await indexQuery.take(100)
const candidate = templates.find((tpl) => tpl.isDefault) ?? null const candidate = templates.find((tpl) => tpl.isDefault) ?? null
if (candidate) { if (candidate) {
return { return {
@ -357,7 +357,7 @@ export const clearCompanyDefault = mutation({
const templates = await ctx.db const templates = await ctx.db
.query("deviceExportTemplates") .query("deviceExportTemplates")
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
.collect() .take(100)
const now = Date.now() const now = Date.now()
await Promise.all( await Promise.all(
templates.map((tpl) => templates.map((tpl) =>

View file

@ -73,11 +73,11 @@ export async function ensureMobileDeviceFields(ctx: MutationCtx, tenantId: strin
const existingMobileFields = await ctx.db const existingMobileFields = await ctx.db
.query("deviceFields") .query("deviceFields")
.withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", "mobile")) .withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", "mobile"))
.collect(); .take(100);
const allFields = await ctx.db const allFields = await ctx.db
.query("deviceFields") .query("deviceFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
const existingByKey = new Map<string, Doc<"deviceFields">>(); const existingByKey = new Map<string, Doc<"deviceFields">>();
existingMobileFields.forEach((field) => existingByKey.set(field.key, field)); existingMobileFields.forEach((field) => existingByKey.set(field.key, field));

View file

@ -64,7 +64,7 @@ export const list = query({
.query("deviceFields") .query("deviceFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
const fields = await fieldsQuery.collect() const fields = await fieldsQuery.take(100)
return fields return fields
.filter((field) => matchesCompany(field.companyId, companyId, true)) .filter((field) => matchesCompany(field.companyId, companyId, true))
.filter((field) => matchesScope(field.scope, scope)) .filter((field) => matchesScope(field.scope, scope))
@ -96,7 +96,7 @@ export const listForTenant = query({
const fields = await ctx.db const fields = await ctx.db
.query("deviceFields") .query("deviceFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return fields return fields
.filter((field) => matchesCompany(field.companyId, companyId, false)) .filter((field) => matchesCompany(field.companyId, companyId, false))
@ -153,7 +153,7 @@ export const create = mutation({
const existing = await ctx.db const existing = await ctx.db
.query("deviceFields") .query("deviceFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", args.tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", args.tenantId))
.collect() .take(100)
const maxOrder = existing.reduce((acc, item) => Math.max(acc, item.order ?? 0), 0) const maxOrder = existing.reduce((acc, item) => Math.max(acc, item.order ?? 0), 0)
const now = Date.now() const now = Date.now()

View file

@ -341,7 +341,7 @@ export const getStats = query({
const all = await ctx.db const all = await ctx.db
.query("emprestimos") .query("emprestimos")
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
.collect() .take(200)
const now = Date.now() const now = Date.now()
const ativos = all.filter((e) => e.status === "ATIVO") const ativos = all.filter((e) => e.status === "ATIVO")

View file

@ -53,7 +53,7 @@ export const list = query({
const fields = await ctx.db const fields = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
return fields return fields
.filter((field) => { .filter((field) => {
@ -87,7 +87,7 @@ export const listForTenant = query({
const fields = await ctx.db const fields = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
return fields return fields
.filter((field) => { .filter((field) => {
@ -157,7 +157,7 @@ export const create = mutation({
const existing = await ctx.db const existing = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
const maxOrder = existing.reduce((acc: number, item: Doc<"ticketFields">) => Math.max(acc, item.order ?? 0), 0); const maxOrder = existing.reduce((acc: number, item: Doc<"ticketFields">) => Math.max(acc, item.order ?? 0), 0);
const now = Date.now(); const now = Date.now();

View file

@ -21,7 +21,7 @@ export const list = query({
.query("incidents") .query("incidents")
.withIndex("by_tenant_updated", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_updated", (q) => q.eq("tenantId", tenantId))
.order("desc") .order("desc")
.collect() .take(100)
return incidents return incidents
}, },
}) })

View file

@ -11,7 +11,7 @@ export const list = query({
const invites = await ctx.db const invites = await ctx.db
.query("userInvites") .query("userInvites")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
return invites return invites
.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)) .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))

View file

@ -81,7 +81,7 @@ async function findActiveMachineToken(ctx: QueryCtx, machineId: Id<"machines">,
.withIndex("by_machine_revoked_expires", (q) => .withIndex("by_machine_revoked_expires", (q) =>
q.eq("machineId", machineId).eq("revoked", false).gt("expiresAt", now), q.eq("machineId", machineId).eq("revoked", false).gt("expiresAt", now),
) )
.collect() .take(100)
return tokens.length > 0 ? tokens[0]! : null return tokens.length > 0 ? tokens[0]! : null
} }
@ -550,7 +550,7 @@ export const register = mutation({
const candidates = await ctx.db const candidates = await ctx.db
.query("machines") .query("machines")
.withIndex("by_tenant_hostname", (q) => q.eq("tenantId", tenantId).eq("hostname", args.hostname)) .withIndex("by_tenant_hostname", (q) => q.eq("tenantId", tenantId).eq("hostname", args.hostname))
.collect() .take(200)
// Procura uma maquina com hostname igual E hardware compativel (MAC ou serial) // Procura uma maquina com hostname igual E hardware compativel (MAC ou serial)
for (const candidate of candidates) { for (const candidate of candidates) {
if (matchesExistingHardware(candidate, identifiers, args.hostname)) { if (matchesExistingHardware(candidate, identifiers, args.hostname)) {
@ -643,7 +643,7 @@ export const register = mutation({
const previousTokens = await ctx.db const previousTokens = await ctx.db
.query("machineTokens") .query("machineTokens")
.withIndex("by_machine", (q) => q.eq("machineId", machineId)) .withIndex("by_machine", (q) => q.eq("machineId", machineId))
.collect() .take(100)
for (const token of previousTokens) { for (const token of previousTokens) {
if (!token.revoked) { if (!token.revoked) {
@ -932,7 +932,7 @@ export const listByTenant = query({
const tenantCompanies = await ctx.db const tenantCompanies = await ctx.db
.query("companies") .query("companies")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(200)
const companyById = new Map<string, typeof tenantCompanies[number]>() const companyById = new Map<string, typeof tenantCompanies[number]>()
const companyBySlug = new Map<string, typeof tenantCompanies[number]>() const companyBySlug = new Map<string, typeof tenantCompanies[number]>()
@ -1574,7 +1574,7 @@ export const listMachineRequesters = query({
const tickets = await ctx.db const tickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", args.machineId)) .withIndex("by_tenant_machine", (q) => q.eq("tenantId", machine.tenantId).eq("machineId", args.machineId))
.collect() .take(200)
const requestersMap = new Map<string, { email: string; name: string | null }>() const requestersMap = new Map<string, { email: string; name: string | null }>()
for (const ticket of tickets) { for (const ticket of tickets) {
@ -2131,7 +2131,7 @@ export const resetAgent = mutation({
const tokens = await ctx.db const tokens = await ctx.db
.query("machineTokens") .query("machineTokens")
.withIndex("by_machine", (q) => q.eq("machineId", machineId)) .withIndex("by_machine", (q) => q.eq("machineId", machineId))
.collect() .take(100)
const now = Date.now() const now = Date.now()
let revokedCount = 0 let revokedCount = 0
@ -2640,7 +2640,7 @@ export const remove = mutation({
const tokens = await ctx.db const tokens = await ctx.db
.query("machineTokens") .query("machineTokens")
.withIndex("by_machine", (q) => q.eq("machineId", machineId)) .withIndex("by_machine", (q) => q.eq("machineId", machineId))
.collect() .take(100)
await Promise.all(tokens.map((token) => ctx.db.delete(token._id))) await Promise.all(tokens.map((token) => ctx.db.delete(token._id)))
await ctx.db.delete(machineId) await ctx.db.delete(machineId)

View file

@ -448,7 +448,7 @@ const metricResolvers: Record<string, MetricResolver> = {
queueCounts.set(queueKey, (queueCounts.get(queueKey) ?? 0) + 1) queueCounts.set(queueKey, (queueCounts.get(queueKey) ?? 0) + 1)
} }
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect() const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).take(50)
const data = Array.from(queueCounts.entries()).map(([queueId, total]) => { const data = Array.from(queueCounts.entries()).map(([queueId, total]) => {
const queue = queues.find((q) => String(q._id) === queueId) const queue = queues.find((q) => String(q._id) === queueId)
return { return {
@ -470,7 +470,7 @@ const metricResolvers: Record<string, MetricResolver> = {
const filterHas = queueFilter && queueFilter.length > 0 const filterHas = queueFilter && queueFilter.length > 0
const normalizeKey = (id: Id<"queues"> | null) => (id ? String(id) : "sem-fila") const normalizeKey = (id: Id<"queues"> | null) => (id ? String(id) : "sem-fila")
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect() const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).take(50)
const queueNameMap = new Map<string, string>() const queueNameMap = new Map<string, string>()
queues.forEach((queue) => { queues.forEach((queue) => {
const key = String(queue._id) const key = String(queue._id)
@ -593,7 +593,7 @@ const metricResolvers: Record<string, MetricResolver> = {
stats.set(queueKey, current) stats.set(queueKey, current)
} }
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect() const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).take(50)
const data = Array.from(stats.entries()).map(([queueId, value]) => { const data = Array.from(stats.entries()).map(([queueId, value]) => {
const queue = queues.find((q) => String(q._id) === queueId) const queue = queues.find((q) => String(q._id) === queueId)
const compliance = value.total > 0 ? value.compliant / value.total : 0 const compliance = value.total > 0 ? value.compliant / value.total : 0

View file

@ -307,21 +307,21 @@ async function getTenantUsers(ctx: QueryCtx, tenantId: string) {
return ctx.db return ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(2000)
} }
async function getTenantQueues(ctx: QueryCtx, tenantId: string) { async function getTenantQueues(ctx: QueryCtx, tenantId: string) {
return ctx.db return ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(500)
} }
async function getTenantCompanies(ctx: QueryCtx, tenantId: string) { async function getTenantCompanies(ctx: QueryCtx, tenantId: string) {
return ctx.db return ctx.db
.query("companies") .query("companies")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(1000)
} }
export const exportTenantSnapshot = query({ export const exportTenantSnapshot = query({
@ -347,7 +347,7 @@ export const exportTenantSnapshot = query({
const tickets = await ctx.db const tickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(5000)
const ticketsWithRelations = [] const ticketsWithRelations = []
@ -355,12 +355,12 @@ export const exportTenantSnapshot = query({
const comments = await ctx.db const comments = await ctx.db
.query("ticketComments") .query("ticketComments")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
.collect() .take(500)
const events = await ctx.db const events = await ctx.db
.query("ticketEvents") .query("ticketEvents")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
.collect() .take(500)
const requester = userMap.get(ticket.requesterId) const requester = userMap.get(ticket.requesterId)
const assignee = ticket.assigneeId ? userMap.get(ticket.assigneeId) : undefined const assignee = ticket.assigneeId ? userMap.get(ticket.assigneeId) : undefined
@ -575,7 +575,7 @@ export const importPrismaSnapshot = mutation({
const existingTenantUsers = await ctx.db const existingTenantUsers = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", snapshot.tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", snapshot.tenantId))
.collect() .take(2000)
for (const user of existingTenantUsers) { for (const user of existingTenantUsers) {
const role = normalizeRole(user.role ?? null) const role = normalizeRole(user.role ?? null)
@ -672,7 +672,7 @@ export const importPrismaSnapshot = mutation({
const existingComments = await ctx.db const existingComments = await ctx.db
.query("ticketComments") .query("ticketComments")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
.collect() .take(500)
for (const comment of existingComments) { for (const comment of existingComments) {
await ctx.db.delete(comment._id) await ctx.db.delete(comment._id)
} }
@ -680,7 +680,7 @@ export const importPrismaSnapshot = mutation({
const existingEvents = await ctx.db const existingEvents = await ctx.db
.query("ticketEvents") .query("ticketEvents")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
.collect() .take(500)
for (const event of existingEvents) { for (const event of existingEvents) {
await ctx.db.delete(event._id) await ctx.db.delete(event._id)
} }
@ -765,7 +765,7 @@ export const backfillTicketCommentAuthorSnapshots = mutation({
const events = await ctx.db const events = await ctx.db
.query("ticketEvents") .query("ticketEvents")
.withIndex("by_ticket", (q) => q.eq("ticketId", comment.ticketId)) .withIndex("by_ticket", (q) => q.eq("ticketId", comment.ticketId))
.collect() .take(100)
const matchingEvent = events.find( const matchingEvent = events.find(
(event) => event.type === "COMMENT_ADDED" && event.createdAt === comment.createdAt, (event) => event.type === "COMMENT_ADDED" && event.createdAt === comment.createdAt,
) )

View file

@ -81,12 +81,12 @@ export const list = query({
const queues = await ctx.db const queues = await ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const teams = await ctx.db const teams = await ctx.db
.query("teams") .query("teams")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return queues.map((queue) => { return queues.map((queue) => {
const team = queue.teamId ? teams.find((item) => item._id === queue.teamId) : null; const team = queue.teamId ? teams.find((item) => item._id === queue.teamId) : null;
@ -109,13 +109,13 @@ export const summary = query({
args: { tenantId: v.string(), viewerId: v.id("users") }, args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => { handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId); await requireStaff(ctx, viewerId, tenantId);
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect(); const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).take(50);
const result = await Promise.all( const result = await Promise.all(
queues.map(async (qItem) => { queues.map(async (qItem) => {
const tickets = await ctx.db const tickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id)) .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id))
.collect(); .take(50);
let pending = 0; let pending = 0;
let inProgress = 0; let inProgress = 0;
let paused = 0; let paused = 0;

View file

@ -509,9 +509,9 @@ async function forEachScopedTicketByResolvedRangeChunked(
.order("desc"); .order("desc");
// Coleta tickets do chunk (o chunk ja e limitado por periodo) // Coleta tickets do chunk (o chunk ja e limitado por periodo)
const snapshot = await query.collect(); const snapshot = await query.take(1000);
// Limita processamento a 1000 tickets por chunk para evitar timeout // Limita processamento a 1000 tickets por chunk para evitar timeout
const limitedSnapshot = snapshot.slice(0, 1000); const limitedSnapshot = snapshot;
for (const ticket of limitedSnapshot) { for (const ticket of limitedSnapshot) {
const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null; const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null;
if (resolvedAt === null) continue; if (resolvedAt === null) continue;
@ -535,11 +535,10 @@ export async function fetchOpenScopedTickets(
// Limita a 500 tickets por status para evitar OOM // Limita a 500 tickets por status para evitar OOM
const MAX_PER_STATUS = 500; const MAX_PER_STATUS = 500;
for (const status of statuses) { for (const status of statuses) {
const allTickets = await ctx.db const snapshot = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_status", (q) => q.eq("tenantId", tenantId).eq("status", status)) .withIndex("by_tenant_status", (q) => q.eq("tenantId", tenantId).eq("status", status))
.collect(); .take(500);
const snapshot = allTickets.slice(0, MAX_PER_STATUS);
for (const ticket of snapshot) { for (const ticket of snapshot) {
if (!OPEN_STATUSES.has(normalizeStatus(ticket.status))) continue; if (!OPEN_STATUSES.has(normalizeStatus(ticket.status))) continue;
if (scopedCompanyId && ticket.companyId !== scopedCompanyId) continue; if (scopedCompanyId && ticket.companyId !== scopedCompanyId) continue;
@ -620,7 +619,7 @@ async function fetchCategoryMap(ctx: QueryCtx, tenantId: string) {
const categories = await ctx.db const categories = await ctx.db
.query("ticketCategories") .query("ticketCategories")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(500);
const map = new Map<string, Doc<"ticketCategories">>(); const map = new Map<string, Doc<"ticketCategories">>();
for (const category of categories) { for (const category of categories) {
map.set(String(category._id), category); map.set(String(category._id), category);
@ -702,7 +701,7 @@ async function fetchQueues(ctx: QueryCtx, tenantId: string) {
return ctx.db return ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(1000);
} }
type CompanySummary = { type CompanySummary = {
@ -1023,7 +1022,7 @@ export async function csatOverviewHandler(
const events = await ctx.db const events = await ctx.db
.query("ticketEvents") .query("ticketEvents")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
.collect(); .take(1000);
for (const event of events) { for (const event of events) {
if (event.type !== "CSAT_RECEIVED" && event.type !== "CSAT_RATED") continue; if (event.type !== "CSAT_RECEIVED" && event.type !== "CSAT_RATED") continue;
@ -1420,11 +1419,10 @@ export async function agentProductivityHandler(
for (const [agentId, acc] of map) { for (const [agentId, acc] of map) {
// Limita a 1000 sessoes por agente para evitar OOM // Limita a 1000 sessoes por agente para evitar OOM
const allSessions = await ctx.db const sessions = await ctx.db
.query("ticketWorkSessions") .query("ticketWorkSessions")
.withIndex("by_agent", (q) => q.eq("agentId", agentId as Id<"users">)) .withIndex("by_agent", (q) => q.eq("agentId", agentId as Id<"users">))
.collect() .take(1000)
const sessions = allSessions.slice(0, 1000)
let total = 0 let total = 0
for (const s of sessions) { for (const s of sessions) {
const started = s.startedAt const started = s.startedAt
@ -1481,7 +1479,7 @@ export async function ticketCategoryInsightsHandler(
const categories = await ctx.db const categories = await ctx.db
.query("ticketCategories") .query("ticketCategories")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(500)
const categoriesById = new Map<Id<"ticketCategories">, Doc<"ticketCategories">>() const categoriesById = new Map<Id<"ticketCategories">, Doc<"ticketCategories">>()
for (const category of categories) { for (const category of categories) {

View file

@ -674,7 +674,8 @@ export default defineSchema({
.index("by_tenant_fingerprint", ["tenantId", "fingerprint"]) .index("by_tenant_fingerprint", ["tenantId", "fingerprint"])
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"]) .index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
.index("by_tenant_hostname", ["tenantId", "hostname"]) .index("by_tenant_hostname", ["tenantId", "hostname"])
.index("by_auth_email", ["authEmail"]), .index("by_auth_email", ["authEmail"])
.index("by_usbPolicyStatus", ["usbPolicyStatus"]),
usbPolicyEvents: defineTable({ usbPolicyEvents: defineTable({
tenantId: v.string(), tenantId: v.string(),

View file

@ -15,7 +15,7 @@ export const seedDemo = mutation({
const existingQueues = await ctx.db const existingQueues = await ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
const normalizedQueues = await Promise.all( const normalizedQueues = await Promise.all(
existingQueues.map(async (queue) => { existingQueues.map(async (queue) => {
@ -135,7 +135,7 @@ export const seedDemo = mutation({
const existingTemplates = await ctx.db const existingTemplates = await ctx.db
.query("commentTemplates") .query("commentTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
for (const definition of templateDefinitions) { for (const definition of templateDefinitions) {
const already = existingTemplates.find((template) => template?.title === definition.title); const already = existingTemplates.find((template) => template?.title === definition.title);

View file

@ -28,7 +28,7 @@ export const list = query({
const items = await ctx.db const items = await ctx.db
.query("slaPolicies") .query("slaPolicies")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return items.map((policy) => ({ return items.map((policy) => ({
id: policy._id, id: policy._id,

View file

@ -28,17 +28,17 @@ export const list = query({
const teams = await ctx.db const teams = await ctx.db
.query("teams") .query("teams")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const queues = await ctx.db const queues = await ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return teams.map((team) => { return teams.map((team) => {
const members = users const members = users
@ -111,7 +111,7 @@ export const update = mutation({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const now = users const now = users
.filter((user) => (user.teams ?? []).includes(team.name)) .filter((user) => (user.teams ?? []).includes(team.name))
@ -150,7 +150,7 @@ export const remove = mutation({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
await Promise.all( await Promise.all(
users users
@ -182,7 +182,7 @@ export const setMembers = mutation({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const tenantUserIds = new Set(users.map((user) => user._id)); const tenantUserIds = new Set(users.map((user) => user._id));
for (const memberId of memberIds) { for (const memberId of memberIds) {
if (!tenantUserIds.has(memberId)) { if (!tenantUserIds.has(memberId)) {
@ -218,7 +218,7 @@ export const directory = query({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return users.map((user) => ({ return users.map((user) => ({
id: user._id, id: user._id,

View file

@ -45,7 +45,7 @@ export const list = query({
const settings = await ctx.db const settings = await ctx.db
.query("ticketFormSettings") .query("ticketFormSettings")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect() .take(100)
return settings return settings
.filter((setting) => !normalizedTemplate || setting.template === normalizedTemplate) .filter((setting) => !normalizedTemplate || setting.template === normalizedTemplate)
.map((setting) => ({ .map((setting) => ({
@ -143,7 +143,7 @@ async function findExisting(
const candidates = await ctx.db const candidates = await ctx.db
.query("ticketFormSettings") .query("ticketFormSettings")
.withIndex("by_tenant_template_scope", (q) => q.eq("tenantId", tenantId).eq("template", template).eq("scope", scope)) .withIndex("by_tenant_template_scope", (q) => q.eq("tenantId", tenantId).eq("template", template).eq("scope", scope))
.collect() .take(100)
return candidates.find((setting) => { return candidates.find((setting) => {
if (scope === "tenant") return true if (scope === "tenant") return true

View file

@ -39,7 +39,7 @@ export async function ensureTicketFormTemplatesForTenant(ctx: MutationCtx, tenan
const existing = await ctx.db const existing = await ctx.db
.query("ticketFormTemplates") .query("ticketFormTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
let order = existing.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0); let order = existing.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0);
const now = Date.now(); const now = Date.now();
for (const template of TICKET_FORM_CONFIG) { for (const template of TICKET_FORM_CONFIG) {
@ -102,12 +102,12 @@ async function cloneFieldsFromTemplate(ctx: MutationCtx, tenantId: string, sourc
const sourceFields = await ctx.db const sourceFields = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", sourceKey)) .withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", sourceKey))
.collect(); .take(50);
if (sourceFields.length === 0) return; if (sourceFields.length === 0) return;
const ordered = await ctx.db const ordered = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
let order = ordered.reduce((max, field) => Math.max(max, field.order ?? 0), 0); let order = ordered.reduce((max, field) => Math.max(max, field.order ?? 0), 0);
const now = Date.now(); const now = Date.now();
for (const field of sourceFields.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) { for (const field of sourceFields.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
@ -156,7 +156,7 @@ export const list = query({
const templates = await ctx.db const templates = await ctx.db
.query("ticketFormTemplates") .query("ticketFormTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return templates return templates
.filter((tpl) => includeArchived || tpl.isArchived !== true) .filter((tpl) => includeArchived || tpl.isArchived !== true)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR")) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR"))
@ -174,7 +174,7 @@ export const listActive = query({
const templates = await ctx.db const templates = await ctx.db
.query("ticketFormTemplates") .query("ticketFormTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
return templates return templates
.filter((tpl) => tpl.isArchived !== true) .filter((tpl) => tpl.isArchived !== true)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR")) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR"))
@ -201,7 +201,7 @@ export const create = mutation({
const templates = await ctx.db const templates = await ctx.db
.query("ticketFormTemplates") .query("ticketFormTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(50);
const order = (templates.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0) ?? 0) + 1; const order = (templates.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0) ?? 0) + 1;
const now = Date.now(); const now = Date.now();
const templateId = await ctx.db.insert("ticketFormTemplates", { const templateId = await ctx.db.insert("ticketFormTemplates", {

View file

@ -635,7 +635,7 @@ async function fetchTemplateSummaries(ctx: AnyCtx, tenantId: string): Promise<Te
const templates = await ctx.db const templates = await ctx.db
.query("ticketFormTemplates") .query("ticketFormTemplates")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
if (!templates.length) { if (!templates.length) {
return TICKET_FORM_CONFIG.map((template) => ({ return TICKET_FORM_CONFIG.map((template) => ({
key: template.key, key: template.key,
@ -682,7 +682,7 @@ async function fetchTicketFieldsByScopes(
const allFields = await ctx.db const allFields = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
const addFieldToScope = (scopeKey: string, field: Doc<"ticketFields">) => { const addFieldToScope = (scopeKey: string, field: Doc<"ticketFields">) => {
const originalKey = scopeLookup.get(scopeKey); const originalKey = scopeLookup.get(scopeKey);
@ -746,7 +746,7 @@ async function fetchViewerScopedFormSettings(
const allSettings = await ctx.db const allSettings = await ctx.db
.query("ticketFormSettings") .query("ticketFormSettings")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
for (const setting of allSettings) { for (const setting of allSettings) {
if (!keySet.has(setting.template)) { if (!keySet.has(setting.template)) {
@ -813,7 +813,7 @@ async function ensureTicketFormDefaultsForTenant(ctx: MutationCtx, tenantId: str
const existing = await ctx.db const existing = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", template.key)) .withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", template.key))
.collect(); .take(100);
if (template.key === "admissao") { if (template.key === "admissao") {
for (const key of OPTIONAL_ADMISSION_FIELD_KEYS) { for (const key of OPTIONAL_ADMISSION_FIELD_KEYS) {
const field = existing.find((f) => f.key === key); const field = existing.find((f) => f.key === key);
@ -1026,7 +1026,7 @@ async function computeAgentWorkTotals(
const sessions = await ctx.db const sessions = await ctx.db
.query("ticketWorkSessions") .query("ticketWorkSessions")
.withIndex("by_ticket", (q) => q.eq("ticketId", ticketId)) .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
.collect(); .take(50);
if (!sessions.length) { if (!sessions.length) {
return []; return [];
@ -1435,7 +1435,7 @@ async function normalizeCustomFieldValues(
const definitions = await ctx.db const definitions = await ctx.db
.query("ticketFields") .query("ticketFields")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(100);
const scopedDefinitions = definitions.filter((definition) => { const scopedDefinitions = definitions.filter((definition) => {
const fieldScope = (definition.scope ?? "all").toLowerCase(); const fieldScope = (definition.scope ?? "all").toLowerCase();
@ -1951,7 +1951,7 @@ export const getById = query({
const comments = await ctx.db const comments = await ctx.db
.query("ticketComments") .query("ticketComments")
.withIndex("by_ticket", (q) => q.eq("ticketId", id)) .withIndex("by_ticket", (q) => q.eq("ticketId", id))
.collect(); .take(50);
const canViewInternalComments = role === "ADMIN" || role === "AGENT"; const canViewInternalComments = role === "ADMIN" || role === "AGENT";
const visibleComments = canViewInternalComments const visibleComments = canViewInternalComments
? comments ? comments
@ -1965,7 +1965,7 @@ export const getById = query({
let timelineRecords = await ctx.db let timelineRecords = await ctx.db
.query("ticketEvents") .query("ticketEvents")
.withIndex("by_ticket", (q) => q.eq("ticketId", id)) .withIndex("by_ticket", (q) => q.eq("ticketId", id))
.collect(); .take(50);
if (!(role === "ADMIN" || role === "AGENT")) { if (!(role === "ADMIN" || role === "AGENT")) {
timelineRecords = timelineRecords.filter((event) => { timelineRecords = timelineRecords.filter((event) => {
@ -2317,7 +2317,7 @@ export const create = mutation({
const queues = await ctx.db const queues = await ctx.db
.query("queues") .query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
.collect() .take(100)
const preferred = queues.find((q) => q.slug === "chamados") || queues.find((q) => q.name === "Chamados") || null const preferred = queues.find((q) => q.slug === "chamados") || queues.find((q) => q.name === "Chamados") || null
if (preferred) { if (preferred) {
resolvedQueueId = preferred._id as Id<"queues"> resolvedQueueId = preferred._id as Id<"queues">
@ -3085,7 +3085,7 @@ export const listChatMessages = query({
const messages = await ctx.db const messages = await ctx.db
.query("ticketChatMessages") .query("ticketChatMessages")
.withIndex("by_ticket_created", (q) => q.eq("ticketId", ticketId)) .withIndex("by_ticket_created", (q) => q.eq("ticketId", ticketId))
.collect() .take(50)
// Verificar maquina e sessao de chat ao vivo // Verificar maquina e sessao de chat ao vivo
let liveChat: { let liveChat: {

View file

@ -196,12 +196,13 @@ export const listUsbPolicyEvents = query({
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const limit = args.limit ?? 10 const limit = args.limit ?? 10
const maxFetch = 1000 // Limite maximo de eventos a buscar
let events = await ctx.db let events = await ctx.db
.query("usbPolicyEvents") .query("usbPolicyEvents")
.withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId)) .withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId))
.order("desc") .order("desc")
.collect() .take(maxFetch)
// Aplica filtro de cursor (paginacao) // Aplica filtro de cursor (paginacao)
if (args.cursor !== undefined) { if (args.cursor !== undefined) {
@ -313,13 +314,11 @@ export const cleanupStalePendingPolicies = mutation({
const cutoff = now - thresholdMs const cutoff = now - thresholdMs
// Buscar maquinas com status PENDING e appliedAt antigo // Buscar maquinas com status PENDING e appliedAt antigo
const allMachines = await ctx.db.query("machines").collect() const staleMachines = await ctx.db
const staleMachines = allMachines.filter( .query("machines")
(m) => .withIndex("by_usbPolicyStatus", (q) => q.eq("usbPolicyStatus", "PENDING"))
m.usbPolicyStatus === "PENDING" && .filter((q) => q.lt(q.field("usbPolicyAppliedAt"), cutoff))
m.usbPolicyAppliedAt !== undefined && .take(1000)
m.usbPolicyAppliedAt < cutoff
)
let cleaned = 0 let cleaned = 0
for (const machine of staleMachines) { for (const machine of staleMachines) {
@ -346,6 +345,6 @@ export const cleanupStalePendingPolicies = mutation({
cleaned++ cleaned++
} }
return { cleaned, checked: allMachines.length } return { cleaned, checked: staleMachines.length }
}, },
}) })

View file

@ -103,7 +103,7 @@ export const listAgents = query({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(5000);
// Only internal staff (ADMIN/AGENT) should appear as responsáveis // Only internal staff (ADMIN/AGENT) should appear as responsáveis
return users return users
@ -128,7 +128,7 @@ export const listCustomers = query({
const users = await ctx.db const users = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect(); .take(5000);
const allowed = users.filter((user) => { const allowed = users.filter((user) => {
const role = (user.role ?? "COLLABORATOR").toUpperCase() const role = (user.role ?? "COLLABORATOR").toUpperCase()
@ -215,7 +215,7 @@ export const deleteUser = mutation({
const comments = await ctx.db const comments = await ctx.db
.query("ticketComments") .query("ticketComments")
.withIndex("by_author", (q) => q.eq("authorId", userId)) .withIndex("by_author", (q) => q.eq("authorId", userId))
.collect(); .take(10000);
if (comments.length > 0) { if (comments.length > 0) {
const authorSnapshot = { const authorSnapshot = {
name: user.name, name: user.name,
@ -243,7 +243,7 @@ export const deleteUser = mutation({
const requesterTickets = await ctx.db const requesterTickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_requester", (q) => q.eq("tenantId", user.tenantId).eq("requesterId", userId)) .withIndex("by_tenant_requester", (q) => q.eq("tenantId", user.tenantId).eq("requesterId", userId))
.collect(); .take(10000);
if (requesterTickets.length > 0) { if (requesterTickets.length > 0) {
const requesterSnapshot = { const requesterSnapshot = {
name: user.name, name: user.name,
@ -267,7 +267,7 @@ export const deleteUser = mutation({
const directReports = await ctx.db const directReports = await ctx.db
.query("users") .query("users")
.withIndex("by_tenant_manager", (q) => q.eq("tenantId", user.tenantId).eq("managerId", userId)) .withIndex("by_tenant_manager", (q) => q.eq("tenantId", user.tenantId).eq("managerId", userId))
.collect(); .take(1000);
await Promise.all( await Promise.all(
directReports.map(async (report) => { directReports.map(async (report) => {
await ctx.db.patch(report._id, { managerId: undefined }); await ctx.db.patch(report._id, { managerId: undefined });

View file

@ -906,6 +906,8 @@ export type DevicesQueryItem = {
linkedUsers?: Array<{ id: string; email: string; name: string }> linkedUsers?: Array<{ id: string; email: string; name: string }>
remoteAccessEntries: DeviceRemoteAccessEntry[] remoteAccessEntries: DeviceRemoteAccessEntry[]
customFields?: Array<{ fieldId?: string; fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }> customFields?: Array<{ fieldId?: string; fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }>
usbPolicy?: "ALLOW_ALL" | "BLOCK_ALL" | "WHITELIST" | null
usbPolicyStatus?: "IDLE" | "PENDING" | "APPLYING" | "APPLIED" | "FAILED" | null
} }
export function normalizeDeviceItem(raw: Record<string, unknown>): DevicesQueryItem { export function normalizeDeviceItem(raw: Record<string, unknown>): DevicesQueryItem {

View file

@ -78,7 +78,10 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
queuesEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" queuesEnabled ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) )
const queues: TicketQueueSummary[] = Array.isArray(queuesResult) ? queuesResult : [] const queues: TicketQueueSummary[] = Array.isArray(queuesResult) ? queuesResult : []
const agents = useQuery(api.users.listAgents, { tenantId }) as { _id: string; name: string }[] | undefined const agents = useQuery(
api.users.listAgents,
isStaff && convexUserId ? { tenantId } : "skip"
) as { _id: string; name: string }[] | undefined
// Argumentos para a query paginada de tickets // Argumentos para a query paginada de tickets
const ticketsArgs = useMemo(() => { const ticketsArgs = useMemo(() => {

View file

@ -6,7 +6,10 @@ import { api } from "@/convex/_generated/api"
import type { TicketCategory } from "@/lib/schemas/category" import type { TicketCategory } from "@/lib/schemas/category"
export function useTicketCategories(tenantId: string) { export function useTicketCategories(tenantId: string) {
const categories = useQuery(api.categories.list, { tenantId }) as TicketCategory[] | undefined const categories = useQuery(
api.categories.list,
tenantId ? { tenantId } : "skip"
) as TicketCategory[] | undefined
const ensureDefaults = useMutation(api.categories.ensureDefaults) const ensureDefaults = useMutation(api.categories.ensureDefaults)
const initializingRef = useRef(false) const initializingRef = useRef(false)

View file

@ -80,7 +80,7 @@ services:
start_period: 180s start_period: 180s
convex_backend: convex_backend:
image: sistema_convex_backend:1.29.2 image: ghcr.io/get-convex/convex-backend:precompiled-2025-12-04-cc6af4c
stop_grace_period: 10s stop_grace_period: 10s
stop_signal: SIGINT stop_signal: SIGINT
volumes: volumes: