feat: add health dashboard and local ticket archive
This commit is contained in:
parent
0d78abbb6f
commit
0a6b808d99
15 changed files with 824 additions and 60 deletions
73
src/server/archive/local-tickets.ts
Normal file
73
src/server/archive/local-tickets.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { mkdir, writeFile } from "fs/promises"
|
||||
import { join, dirname } from "path"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { env } from "@/lib/env"
|
||||
import { createConvexClient } from "@/server/convex-client"
|
||||
|
||||
type ArchiveItem = {
|
||||
ticket: Record<string, unknown>
|
||||
comments: Array<Record<string, unknown>>
|
||||
events: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
type ExportResponse = {
|
||||
total: number
|
||||
items: ArchiveItem[]
|
||||
}
|
||||
|
||||
function assertArchiveSecret(): string {
|
||||
const secret = env.INTERNAL_HEALTH_TOKEN ?? env.REPORTS_CRON_SECRET
|
||||
if (!secret) {
|
||||
throw new Error("Defina INTERNAL_HEALTH_TOKEN ou REPORTS_CRON_SECRET para exportar tickets")
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-")
|
||||
}
|
||||
|
||||
export async function exportResolvedTicketsToDisk(options?: {
|
||||
days?: number
|
||||
limit?: number
|
||||
tenantId?: string
|
||||
}) {
|
||||
const days = options?.days ?? 365
|
||||
const limit = options?.limit ?? 50
|
||||
const tenantId = options?.tenantId ?? DEFAULT_TENANT_ID
|
||||
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
|
||||
const secret = assertArchiveSecret()
|
||||
const client = createConvexClient()
|
||||
|
||||
// @ts-expect-error - exportForArchive é adicionada manualmente sem regen do client
|
||||
const res = (await client.query(api.tickets.exportForArchive, {
|
||||
tenantId,
|
||||
before: cutoff,
|
||||
limit,
|
||||
secret,
|
||||
})) as ExportResponse
|
||||
|
||||
const archiveDir = env.ARCHIVE_DIR ?? "./archives"
|
||||
const filename = `tickets-archive-${nowIso()}-resolved-${days}d.jsonl`
|
||||
const fullPath = join(archiveDir, filename)
|
||||
await mkdir(dirname(fullPath), { recursive: true })
|
||||
|
||||
const lines = res.items.map((item) =>
|
||||
JSON.stringify({
|
||||
ticketId: item.ticket?._id ?? null,
|
||||
tenantId,
|
||||
archivedAt: Date.now(),
|
||||
ticket: item.ticket,
|
||||
comments: item.comments,
|
||||
events: item.events,
|
||||
})
|
||||
)
|
||||
await writeFile(fullPath, lines.join("\n"), { encoding: "utf-8" })
|
||||
|
||||
return {
|
||||
written: res.items.length,
|
||||
file: fullPath,
|
||||
}
|
||||
}
|
||||
98
src/server/health.ts
Normal file
98
src/server/health.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { api } from "@/convex/_generated/api"
|
||||
import { RETENTION_POLICY, RETENTION_STRATEGY } from "@/lib/retention"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { createConvexClient } from "@/server/convex-client"
|
||||
import { env } from "@/lib/env"
|
||||
|
||||
const OPEN_TICKET_STATUSES = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED"]
|
||||
|
||||
type DeviceHealth = {
|
||||
machines: number
|
||||
online: number
|
||||
warning: number
|
||||
offline: number
|
||||
withoutHeartbeat: number
|
||||
newestHeartbeatAgeMs: number | null
|
||||
oldestHeartbeatAgeMs: number | null
|
||||
thresholds: {
|
||||
offlineMs: number
|
||||
staleMs: number
|
||||
}
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
export type HealthSnapshot = {
|
||||
generatedAt: string
|
||||
tickets: {
|
||||
total: number
|
||||
open: number
|
||||
last7d: number
|
||||
last24h: number
|
||||
}
|
||||
accounts: {
|
||||
users: number
|
||||
companies: number
|
||||
}
|
||||
devices: DeviceHealth | null
|
||||
retention: typeof RETENTION_POLICY
|
||||
retentionStrategy: typeof RETENTION_STRATEGY
|
||||
notes?: string
|
||||
}
|
||||
|
||||
function toIsoString(date: Date) {
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
export async function getHealthSnapshot(): Promise<HealthSnapshot> {
|
||||
const now = new Date()
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const [totalTickets, openTickets, lastWeekTickets, lastDayTickets, userCount, companyCount] = await Promise.all([
|
||||
prisma.ticket.count(),
|
||||
prisma.ticket.count({ where: { status: { in: OPEN_TICKET_STATUSES } } }),
|
||||
prisma.ticket.count({ where: { createdAt: { gte: sevenDaysAgo } } }),
|
||||
prisma.ticket.count({ where: { createdAt: { gte: oneDayAgo } } }),
|
||||
prisma.user.count(),
|
||||
prisma.company.count(),
|
||||
])
|
||||
|
||||
let devices: DeviceHealth | null = null
|
||||
try {
|
||||
const client = createConvexClient()
|
||||
const convexHealth = await client.query(api.ops.healthSnapshot, {
|
||||
token: env.INTERNAL_HEALTH_TOKEN ?? env.REPORTS_CRON_SECRET ?? undefined,
|
||||
})
|
||||
devices = {
|
||||
machines: convexHealth.totals.machines,
|
||||
online: convexHealth.connectivity.online,
|
||||
warning: convexHealth.connectivity.warning,
|
||||
offline: convexHealth.connectivity.offline,
|
||||
withoutHeartbeat: convexHealth.totals.withoutHeartbeat,
|
||||
newestHeartbeatAgeMs: convexHealth.heartbeatAgeMs.newest,
|
||||
oldestHeartbeatAgeMs: convexHealth.heartbeatAgeMs.oldest,
|
||||
thresholds: convexHealth.thresholds,
|
||||
truncated: convexHealth.totals.truncated,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[health] Falha ao carregar estado das maquinas", error)
|
||||
}
|
||||
|
||||
return {
|
||||
generatedAt: toIsoString(now),
|
||||
tickets: {
|
||||
total: totalTickets,
|
||||
open: openTickets,
|
||||
last7d: lastWeekTickets,
|
||||
last24h: lastDayTickets,
|
||||
},
|
||||
accounts: {
|
||||
users: userCount,
|
||||
companies: companyCount,
|
||||
},
|
||||
devices,
|
||||
retention: RETENTION_POLICY,
|
||||
retentionStrategy: RETENTION_STRATEGY,
|
||||
notes: devices ? undefined : "Convex nao respondeu; verificar conectividade ou token interno.",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue