chore: sync staging

This commit is contained in:
Esdras Renan 2025-11-10 01:57:45 -03:00
parent c5ddd54a3e
commit 561b19cf66
610 changed files with 105285 additions and 1206 deletions

178
convex/incidents.ts Normal file
View file

@ -0,0 +1,178 @@
import { ConvexError, v } from "convex/values"
import { mutation, query } from "./_generated/server"
import type { Id } from "./_generated/dataModel"
import { requireStaff } from "./rbac"
const DEFAULT_STATUS = "investigating"
function timelineId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
export const list = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId)
const incidents = await ctx.db
.query("incidents")
.withIndex("by_tenant_updated", (q) => q.eq("tenantId", tenantId))
.order("desc")
.collect()
return incidents
},
})
export const createIncident = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
title: v.string(),
severity: v.string(),
impactSummary: v.optional(v.string()),
affectedQueues: v.optional(v.array(v.string())),
initialUpdate: v.optional(v.string()),
},
handler: async (ctx, { tenantId, actorId, title, severity, impactSummary, affectedQueues, initialUpdate }) => {
const viewer = await requireStaff(ctx, actorId, tenantId)
const normalizedTitle = title.trim()
if (normalizedTitle.length < 3) {
throw new ConvexError("Informe um título válido para o incidente")
}
const now = Date.now()
const timelineEntry = {
id: timelineId(),
authorId: actorId,
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
message: initialUpdate?.trim().length ? initialUpdate.trim() : "Incidente registrado.",
type: "created",
createdAt: now,
}
const id = await ctx.db.insert("incidents", {
tenantId,
title: normalizedTitle,
status: DEFAULT_STATUS,
severity,
impactSummary: impactSummary?.trim() || undefined,
affectedQueues: affectedQueues ?? [],
ownerId: actorId,
ownerName: viewer.user.name ?? undefined,
ownerEmail: viewer.user.email ?? undefined,
startedAt: now,
updatedAt: now,
resolvedAt: undefined,
timeline: [timelineEntry],
})
return id
},
})
export const updateIncidentStatus = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
incidentId: v.id("incidents"),
status: v.string(),
},
handler: async (ctx, { tenantId, actorId, incidentId, status }) => {
const viewer = await requireStaff(ctx, actorId, tenantId)
const incident = await ctx.db.get(incidentId)
if (!incident || incident.tenantId !== tenantId) {
throw new ConvexError("Incidente não encontrado")
}
const now = Date.now()
const timeline = [
...(incident.timeline ?? []),
{
id: timelineId(),
authorId: actorId,
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
message: `Status atualizado para ${status}`,
type: "status",
createdAt: now,
},
]
await ctx.db.patch(incidentId, {
status,
updatedAt: now,
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
timeline,
})
},
})
export const bulkUpdateIncidentStatus = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
incidentIds: v.array(v.id("incidents")),
status: v.string(),
},
handler: async (ctx, { tenantId, actorId, incidentIds, status }) => {
const viewer = await requireStaff(ctx, actorId, tenantId)
const now = Date.now()
for (const incidentId of incidentIds) {
const incident = await ctx.db.get(incidentId)
if (!incident || incident.tenantId !== tenantId) continue
const timeline = [
...(incident.timeline ?? []),
{
id: timelineId(),
authorId: actorId,
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
message: `Status atualizado em massa para ${status}`,
type: "status",
createdAt: now,
},
]
await ctx.db.patch(incidentId, {
status,
updatedAt: now,
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
timeline,
})
}
},
})
export const addIncidentUpdate = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
incidentId: v.id("incidents"),
message: v.string(),
status: v.optional(v.string()),
},
handler: async (ctx, { tenantId, actorId, incidentId, message, status }) => {
const viewer = await requireStaff(ctx, actorId, tenantId)
const incident = await ctx.db.get(incidentId)
if (!incident || incident.tenantId !== tenantId) {
throw new ConvexError("Incidente não encontrado")
}
const trimmed = message.trim()
if (trimmed.length < 3) {
throw new ConvexError("Descreva a atualização do incidente")
}
const now = Date.now()
const timeline = [
...(incident.timeline ?? []),
{
id: timelineId(),
authorId: actorId,
authorName: viewer.user.name ?? viewer.user.email ?? "Equipe",
message: trimmed,
type: "update",
createdAt: now,
},
]
await ctx.db.patch(incidentId, {
timeline,
status: status ?? incident.status,
updatedAt: now,
resolvedAt: status === "resolved" ? now : incident.resolvedAt ?? undefined,
})
},
})