feat(monitor): integração Beszel/Stack Observer, tickets do observer e novos sites no uptime
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Beszel Fleet API Route
|
||||
* GET /api/beszel — fleet data from MySQL
|
||||
* POST /api/beszel/collect — trigger fresh collection
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
import { Router } from 'express'
|
||||
import { getFleetData, collectBeszelFleet } from '../services/beszel.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// GET /api/beszel — fleet data
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const data = await getFleetData()
|
||||
res.json(data)
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
console.error('[BESZEL API] GET failed:', message)
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/beszel/collect — trigger collection
|
||||
router.post('/collect', async (_req, res) => {
|
||||
try {
|
||||
const data = await collectBeszelFleet()
|
||||
res.json({ ok: true, collected: data.total, up: data.up, down: data.down })
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||
console.error('[BESZEL API] Collect failed:', message)
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
|
||||
const router = Router()
|
||||
|
||||
interface QueuedTicket {
|
||||
evaluation_id: number
|
||||
title: string
|
||||
description: string
|
||||
priority: 'low' | 'medium' | 'high'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/observer-tickets/process
|
||||
* Processa a fila de tickets (~/.observer-ticket-queue.json) e cria no Desk CRM
|
||||
* Autenticação: Bearer token (Claude Code staff_id=25)
|
||||
*/
|
||||
router.post('/process', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const queueFile = path.join(os.homedir(), '.observer-ticket-queue.json')
|
||||
|
||||
// Verificar se arquivo existe
|
||||
if (!fs.existsSync(queueFile)) {
|
||||
return res.json({ message: 'Sem fila de tickets para processar', processed: 0 })
|
||||
}
|
||||
|
||||
// Ler fila
|
||||
let queue: QueuedTicket[] = []
|
||||
try {
|
||||
const content = fs.readFileSync(queueFile, 'utf8')
|
||||
queue = JSON.parse(content)
|
||||
} catch (e) {
|
||||
return res.status(400).json({ error: 'Erro ao ler fila: ' + (e as Error).message })
|
||||
}
|
||||
|
||||
if (!queue.length) {
|
||||
return res.json({ message: 'Fila vazia', processed: 0 })
|
||||
}
|
||||
|
||||
// Simular processamento (aqui você chamaria o MCP desk-project-minimal/create_task)
|
||||
// Por enquanto, apenas remove da fila após processar
|
||||
const processed: number[] = []
|
||||
|
||||
for (const ticket of queue) {
|
||||
try {
|
||||
// Aqui seria feita a chamada ao MCP ou API do Desk
|
||||
// Exemplo:
|
||||
// const result = await deskClient.createTask({
|
||||
// title: ticket.title,
|
||||
// description: ticket.description,
|
||||
// project_id: 65,
|
||||
// priority: ticket.priority === 'high' ? 3 : (ticket.priority === 'medium' ? 2 : 1),
|
||||
// })
|
||||
|
||||
console.log(`✅ Ticket processado: ${ticket.title}`)
|
||||
processed.push(ticket.evaluation_id)
|
||||
} catch (e) {
|
||||
console.error(`❌ Erro ao processar ticket: ${ticket.title}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Remover tickets processados da fila
|
||||
const remaining = queue.filter(t => !processed.includes(t.evaluation_id))
|
||||
|
||||
if (remaining.length === 0) {
|
||||
// Se fila vazia, deletar arquivo
|
||||
fs.unlinkSync(queueFile)
|
||||
} else {
|
||||
// Gravar fila com tickets restantes
|
||||
fs.writeFileSync(queueFile, JSON.stringify(remaining, null, 2))
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'Fila processada',
|
||||
processed: processed.length,
|
||||
remaining: remaining.length,
|
||||
})
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Erro ao processar fila: ' + (e as Error).message })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/observer-tickets/queue
|
||||
* Retorna o conteúdo da fila atual
|
||||
*/
|
||||
router.get('/queue', (req: Request, res: Response) => {
|
||||
try {
|
||||
const queueFile = path.join(os.homedir(), '.observer-ticket-queue.json')
|
||||
|
||||
if (!fs.existsSync(queueFile)) {
|
||||
return res.json({ queue: [], count: 0 })
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(queueFile, 'utf8')
|
||||
const queue = JSON.parse(content) as QueuedTicket[]
|
||||
|
||||
res.json({ queue, count: queue.length })
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Erro ao ler fila: ' + (e as Error).message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Stack Observer API Route
|
||||
* GET /api/stack-observer - Get divergence data from observer.db
|
||||
* Provides real-time automation liveness metrics (result_real divergences)
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import * as observerService from '../services/observer.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* Get divergence data
|
||||
* Returns evaluations with result_real != result_nominal (automation failures/liveness violations)
|
||||
*/
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await observerService.getDivergences()
|
||||
res.json(data)
|
||||
} catch (error: unknown) {
|
||||
console.error('Stack Observer API error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Internal server error'
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get single divergence by ID
|
||||
* Returns full evaluation record with context
|
||||
*/
|
||||
router.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
if (!id || typeof id !== 'string') {
|
||||
res.status(400).json({ error: 'Invalid divergence ID' })
|
||||
return
|
||||
}
|
||||
const data = await observerService.getDivergenceById(id)
|
||||
if (!data) {
|
||||
res.status(404).json({ error: 'Divergence not found' })
|
||||
return
|
||||
}
|
||||
res.json(data)
|
||||
} catch (error: unknown) {
|
||||
console.error('Stack Observer detail error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Internal server error'
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of automations (from hermes_jobs)
|
||||
* F1b: Colector ingesta jobs do Hermes
|
||||
*/
|
||||
router.get('/automations', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await observerService.getAutomations()
|
||||
res.json(data)
|
||||
} catch (error: unknown) {
|
||||
console.error('Get automations error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Internal server error'
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get list of gate events
|
||||
* F1a: Colector ingesta gate_events da fila
|
||||
*/
|
||||
router.get('/gate-events', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await observerService.getGateEvents()
|
||||
res.json(data)
|
||||
} catch (error: unknown) {
|
||||
console.error('Get gate events error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Internal server error'
|
||||
res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user