All checks were successful
- Agente Rust: captura LastBootTime, uptime e contagem de boots - Backend: extrai campos do extended (bootInfo, discos, RAM, etc) antes de salvar - Frontend /devices: exibe secao de ultimo reinicio - SLA global: adiciona campos de modo, threshold de alerta e status de pausa - Corrige acento em "destinatario" -> "destinatario" em automations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
import { mutation, query } from "./_generated/server";
|
|
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|
import { ConvexError, v } from "convex/values";
|
|
import type { Id } from "./_generated/dataModel";
|
|
|
|
import { requireAdmin } from "./rbac";
|
|
|
|
function normalizeName(value: string) {
|
|
return value.trim();
|
|
}
|
|
|
|
function normalizeMode(value?: string): "business" | "calendar" {
|
|
if (value === "business") return "business";
|
|
return "calendar";
|
|
}
|
|
|
|
function normalizeThreshold(value?: number): number {
|
|
if (value === undefined || value === null) return 0.8;
|
|
if (value < 0.1) return 0.1;
|
|
if (value > 0.95) return 0.95;
|
|
return value;
|
|
}
|
|
|
|
const VALID_PAUSE_STATUSES = ["PAUSED", "PENDING", "AWAITING_ATTENDANCE"] as const;
|
|
|
|
function normalizePauseStatuses(statuses?: string[]): string[] {
|
|
if (!statuses || statuses.length === 0) return ["PAUSED"];
|
|
const filtered = statuses.filter((s) => VALID_PAUSE_STATUSES.includes(s as typeof VALID_PAUSE_STATUSES[number]));
|
|
return filtered.length > 0 ? filtered : ["PAUSED"];
|
|
}
|
|
|
|
type AnyCtx = QueryCtx | MutationCtx;
|
|
|
|
async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"slaPolicies">) {
|
|
const existing = await ctx.db
|
|
.query("slaPolicies")
|
|
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name))
|
|
.first();
|
|
if (existing && (!excludeId || existing._id !== excludeId)) {
|
|
throw new ConvexError("Já existe uma política SLA com este nome");
|
|
}
|
|
}
|
|
|
|
export const list = query({
|
|
args: { tenantId: v.string(), viewerId: v.id("users") },
|
|
handler: async (ctx, { tenantId, viewerId }) => {
|
|
await requireAdmin(ctx, viewerId, tenantId);
|
|
const items = await ctx.db
|
|
.query("slaPolicies")
|
|
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
return items.map((policy) => ({
|
|
id: policy._id,
|
|
name: policy.name,
|
|
description: policy.description ?? "",
|
|
timeToFirstResponse: policy.timeToFirstResponse ?? null,
|
|
responseMode: policy.responseMode ?? "calendar",
|
|
timeToResolution: policy.timeToResolution ?? null,
|
|
solutionMode: policy.solutionMode ?? "calendar",
|
|
alertThreshold: policy.alertThreshold ?? 0.8,
|
|
pauseStatuses: policy.pauseStatuses ?? ["PAUSED"],
|
|
}));
|
|
},
|
|
});
|
|
|
|
export const create = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
timeToFirstResponse: v.optional(v.number()),
|
|
responseMode: v.optional(v.string()),
|
|
timeToResolution: v.optional(v.number()),
|
|
solutionMode: v.optional(v.string()),
|
|
alertThreshold: v.optional(v.number()),
|
|
pauseStatuses: v.optional(v.array(v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const { tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args;
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const trimmed = normalizeName(name);
|
|
if (trimmed.length < 2) {
|
|
throw new ConvexError("Informe um nome válido para a política");
|
|
}
|
|
await ensureUniqueName(ctx, tenantId, trimmed);
|
|
if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) {
|
|
throw new ConvexError("Tempo para primeira resposta deve ser positivo");
|
|
}
|
|
if (timeToResolution !== undefined && timeToResolution < 0) {
|
|
throw new ConvexError("Tempo para resolução deve ser positivo");
|
|
}
|
|
|
|
const id = await ctx.db.insert("slaPolicies", {
|
|
tenantId,
|
|
name: trimmed,
|
|
description,
|
|
timeToFirstResponse,
|
|
responseMode: normalizeMode(responseMode),
|
|
timeToResolution,
|
|
solutionMode: normalizeMode(solutionMode),
|
|
alertThreshold: normalizeThreshold(alertThreshold),
|
|
pauseStatuses: normalizePauseStatuses(pauseStatuses),
|
|
});
|
|
return id;
|
|
},
|
|
});
|
|
|
|
export const update = mutation({
|
|
args: {
|
|
policyId: v.id("slaPolicies"),
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
timeToFirstResponse: v.optional(v.number()),
|
|
responseMode: v.optional(v.string()),
|
|
timeToResolution: v.optional(v.number()),
|
|
solutionMode: v.optional(v.string()),
|
|
alertThreshold: v.optional(v.number()),
|
|
pauseStatuses: v.optional(v.array(v.string())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const { policyId, tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args;
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const policy = await ctx.db.get(policyId);
|
|
if (!policy || policy.tenantId !== tenantId) {
|
|
throw new ConvexError("Política não encontrada");
|
|
}
|
|
const trimmed = normalizeName(name);
|
|
if (trimmed.length < 2) {
|
|
throw new ConvexError("Informe um nome válido para a política");
|
|
}
|
|
if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) {
|
|
throw new ConvexError("Tempo para primeira resposta deve ser positivo");
|
|
}
|
|
if (timeToResolution !== undefined && timeToResolution < 0) {
|
|
throw new ConvexError("Tempo para resolução deve ser positivo");
|
|
}
|
|
await ensureUniqueName(ctx, tenantId, trimmed, policyId);
|
|
|
|
await ctx.db.patch(policyId, {
|
|
name: trimmed,
|
|
description,
|
|
timeToFirstResponse,
|
|
responseMode: normalizeMode(responseMode),
|
|
timeToResolution,
|
|
solutionMode: normalizeMode(solutionMode),
|
|
alertThreshold: normalizeThreshold(alertThreshold),
|
|
pauseStatuses: normalizePauseStatuses(pauseStatuses),
|
|
});
|
|
},
|
|
});
|
|
|
|
export const remove = mutation({
|
|
args: {
|
|
policyId: v.id("slaPolicies"),
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
},
|
|
handler: async (ctx, { policyId, tenantId, actorId }) => {
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const policy = await ctx.db.get(policyId);
|
|
if (!policy || policy.tenantId !== tenantId) {
|
|
throw new ConvexError("Política não encontrada");
|
|
}
|
|
|
|
const ticketLinked = await ctx.db
|
|
.query("tickets")
|
|
.withIndex("by_tenant_sla_policy", (q) => q.eq("tenantId", tenantId).eq("slaPolicyId", policyId))
|
|
.first();
|
|
if (ticketLinked) {
|
|
throw new ConvexError("Remova a associação de tickets antes de excluir a política");
|
|
}
|
|
|
|
await ctx.db.delete(policyId);
|
|
},
|
|
});
|