Compare commits
23
Commits
+46
-11
@@ -18,13 +18,19 @@ TOKEN=""
|
|||||||
API_URL=""
|
API_URL=""
|
||||||
INTERVAL=30
|
INTERVAL=30
|
||||||
INSTALL_DIR=""
|
INSTALL_DIR=""
|
||||||
|
CPU_THRESHOLD=90
|
||||||
|
RENICE_ENABLED=true
|
||||||
|
RENICE_CONSECUTIVE=2
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
--token=*) TOKEN="${arg#*=}" ;;
|
--token=*) TOKEN="${arg#*=}" ;;
|
||||||
--url=*) API_URL="${arg#*=}" ;;
|
--url=*) API_URL="${arg#*=}" ;;
|
||||||
--interval=*) INTERVAL="${arg#*=}" ;;
|
--interval=*) INTERVAL="${arg#*=}" ;;
|
||||||
--dir=*) INSTALL_DIR="${arg#*=}" ;;
|
--dir=*) INSTALL_DIR="${arg#*=}" ;;
|
||||||
|
--cpu-threshold=*) CPU_THRESHOLD="${arg#*=}" ;;
|
||||||
|
--renice=*) RENICE_ENABLED="${arg#*=}" ;;
|
||||||
|
--renice-consecutive=*) RENICE_CONSECUTIVE="${arg#*=}" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -74,17 +80,38 @@ info "Directorio: ${INSTALL_DIR}"
|
|||||||
info "Intervalo: ${INTERVAL}s"
|
info "Intervalo: ${INTERVAL}s"
|
||||||
|
|
||||||
# ── Instalar binario ──────────────────────────────────────────────────────────
|
# ── Instalar binario ──────────────────────────────────────────────────────────
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR" || error "No se pudo crear directorio ${INSTALL_DIR} (¿permisos?)"
|
||||||
|
|
||||||
|
# Verificar espacio disponible (necesita al menos 20 MB)
|
||||||
|
AVAIL_KB=$(df -k "$INSTALL_DIR" 2>/dev/null | awk 'NR==2 {print $4}')
|
||||||
|
if [[ -n "$AVAIL_KB" && "$AVAIL_KB" -lt 20480 ]]; then
|
||||||
|
error "Espacio insuficiente en ${INSTALL_DIR}: solo ${AVAIL_KB} KB disponibles (se necesitan ≥20 MB)"
|
||||||
|
fi
|
||||||
|
|
||||||
info "Descargando agente desde ${DOWNLOAD_URL}..."
|
info "Descargando agente desde ${DOWNLOAD_URL}..."
|
||||||
|
DEST="${INSTALL_DIR}/usite-agent"
|
||||||
|
TMP_DEST="/tmp/usite-agent-download-$$"
|
||||||
|
|
||||||
if command -v curl &>/dev/null; then
|
if command -v curl &>/dev/null; then
|
||||||
curl -fsSL "$DOWNLOAD_URL" -o "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar"
|
curl -fsSL "$DOWNLOAD_URL" -o "$TMP_DEST" 2>/tmp/curl_err || {
|
||||||
|
CURL_ERR=$(cat /tmp/curl_err 2>/dev/null)
|
||||||
|
rm -f "$TMP_DEST"
|
||||||
|
error "No se pudo descargar: ${CURL_ERR}"
|
||||||
|
}
|
||||||
elif command -v wget &>/dev/null; then
|
elif command -v wget &>/dev/null; then
|
||||||
wget -q "$DOWNLOAD_URL" -O "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar"
|
wget -q "$DOWNLOAD_URL" -O "$TMP_DEST" || { rm -f "$TMP_DEST"; error "No se pudo descargar con wget"; }
|
||||||
else
|
else
|
||||||
error "Se requiere curl o wget"
|
error "Se requiere curl o wget"
|
||||||
fi
|
fi
|
||||||
chmod +x "${INSTALL_DIR}/usite-agent"
|
|
||||||
ok "Binario descargado"
|
if [[ ! -s "$TMP_DEST" ]]; then
|
||||||
|
rm -f "$TMP_DEST"
|
||||||
|
error "El archivo descargado está vacío"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$TMP_DEST" "$DEST" || { cp "$TMP_DEST" "$DEST" && rm -f "$TMP_DEST"; } || error "No se pudo mover el binario a ${DEST}"
|
||||||
|
chmod +x "$DEST"
|
||||||
|
ok "Binario descargado ($(du -sh "$DEST" | cut -f1))"
|
||||||
|
|
||||||
# ── Configuración ─────────────────────────────────────────────────────────────
|
# ── Configuración ─────────────────────────────────────────────────────────────
|
||||||
cat > "${INSTALL_DIR}/agent.yml" <<EOF
|
cat > "${INSTALL_DIR}/agent.yml" <<EOF
|
||||||
@@ -92,6 +119,9 @@ api_url: ${API_URL}
|
|||||||
token: ${TOKEN}
|
token: ${TOKEN}
|
||||||
interval: ${INTERVAL}
|
interval: ${INTERVAL}
|
||||||
debug: false
|
debug: false
|
||||||
|
cpu_threshold: ${CPU_THRESHOLD}
|
||||||
|
renice_enabled: ${RENICE_ENABLED}
|
||||||
|
renice_consecutive: ${RENICE_CONSECUTIVE}
|
||||||
EOF
|
EOF
|
||||||
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
|
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
|
||||||
|
|
||||||
@@ -121,8 +151,13 @@ WantedBy=multi-user.target
|
|||||||
EOF
|
EOF
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable usite-agent
|
systemctl enable usite-agent
|
||||||
systemctl start usite-agent
|
if systemctl is-active --quiet usite-agent; then
|
||||||
ok "Servicio systemd 'usite-agent' instalado y activo"
|
systemctl restart usite-agent
|
||||||
|
ok "Servicio systemd 'usite-agent' reiniciado con nueva configuración"
|
||||||
|
else
|
||||||
|
systemctl start usite-agent
|
||||||
|
ok "Servicio systemd 'usite-agent' instalado y activo"
|
||||||
|
fi
|
||||||
STARTED=1
|
STARTED=1
|
||||||
|
|
||||||
# 2) openrc (Alpine, algunos VPS)
|
# 2) openrc (Alpine, algunos VPS)
|
||||||
|
|||||||
+195
-61
@@ -8,7 +8,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
@@ -16,16 +18,21 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
"github.com/shirou/gopsutil/v3/load"
|
"github.com/shirou/gopsutil/v3/load"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
pnet "github.com/shirou/gopsutil/v3/net"
|
||||||
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Config ────────────────────────────────────────────────────────────────────
|
// ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
APIURL string `yaml:"api_url"` // https://admin.u-site.app
|
APIURL string `yaml:"api_url"`
|
||||||
Token string `yaml:"token"` // token generado desde el panel
|
Token string `yaml:"token"`
|
||||||
Interval int `yaml:"interval"` // segundos entre reportes (default: 30)
|
Interval int `yaml:"interval"`
|
||||||
Debug bool `yaml:"debug"`
|
Debug bool `yaml:"debug"`
|
||||||
|
CPUThreshold float64 `yaml:"cpu_threshold"` // % para activar renice (default 90)
|
||||||
|
ReniceEnabled bool `yaml:"renice_enabled"` // activar renice automático
|
||||||
|
ReniceConsecutive int `yaml:"renice_consecutive"` // reportes consecutivos requeridos (default 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(path string) (*Config, error) {
|
func loadConfig(path string) (*Config, error) {
|
||||||
@@ -40,22 +47,32 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
if cfg.Interval <= 0 {
|
if cfg.Interval <= 0 {
|
||||||
cfg.Interval = 30
|
cfg.Interval = 30
|
||||||
}
|
}
|
||||||
|
if cfg.CPUThreshold <= 0 {
|
||||||
|
cfg.CPUThreshold = 90
|
||||||
|
}
|
||||||
|
if cfg.ReniceConsecutive <= 0 {
|
||||||
|
cfg.ReniceConsecutive = 2
|
||||||
|
}
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Métricas ──────────────────────────────────────────────────────────────────
|
// ── Métricas ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type Metricas struct {
|
type Metricas struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
Arch string `json:"arch"`
|
Arch string `json:"arch"`
|
||||||
Uptime uint64 `json:"uptime_seg"`
|
Uptime uint64 `json:"uptime_seg"`
|
||||||
UptimeStr string `json:"uptime"`
|
UptimeStr string `json:"uptime"`
|
||||||
RAM RAMInfo `json:"ram"`
|
RAM RAMInfo `json:"ram"`
|
||||||
CPU CPUInfo `json:"cpu"`
|
Swap SwapInfo `json:"swap"`
|
||||||
Discos []DiscoInfo `json:"discos"`
|
CPU CPUInfo `json:"cpu"`
|
||||||
LoadAvg LoadInfo `json:"load_avg"`
|
Discos []DiscoInfo `json:"discos"`
|
||||||
ReportadoEn string `json:"reportado_en"`
|
LoadAvg LoadInfo `json:"load_avg"`
|
||||||
|
Red []RedInfo `json:"red"`
|
||||||
|
TCPConns int `json:"tcp_conns"`
|
||||||
|
TopProcs []ProcesoInfo `json:"top_procs"`
|
||||||
|
ReportadoEn string `json:"reportado_en"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RAMInfo struct {
|
type RAMInfo struct {
|
||||||
@@ -65,6 +82,12 @@ type RAMInfo struct {
|
|||||||
Porcentaje float64 `json:"porcentaje"`
|
Porcentaje float64 `json:"porcentaje"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SwapInfo struct {
|
||||||
|
TotalGB float64 `json:"total_gb"`
|
||||||
|
UsadoGB float64 `json:"usado_gb"`
|
||||||
|
Porcentaje float64 `json:"porcentaje"`
|
||||||
|
}
|
||||||
|
|
||||||
type CPUInfo struct {
|
type CPUInfo struct {
|
||||||
Nucleos int `json:"nucleos"`
|
Nucleos int `json:"nucleos"`
|
||||||
Porcentaje float64 `json:"porcentaje"`
|
Porcentaje float64 `json:"porcentaje"`
|
||||||
@@ -84,6 +107,21 @@ type LoadInfo struct {
|
|||||||
Load15 float64 `json:"load15"`
|
Load15 float64 `json:"load15"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedInfo struct {
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
BytesEnvMB float64 `json:"bytes_env_mb"`
|
||||||
|
BytesRecMB float64 `json:"bytes_rec_mb"`
|
||||||
|
PktEnv uint64 `json:"pkt_env"`
|
||||||
|
PktRec uint64 `json:"pkt_rec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcesoInfo struct {
|
||||||
|
PID int32 `json:"pid"`
|
||||||
|
Nombre string `json:"nombre"`
|
||||||
|
CPU float64 `json:"cpu"`
|
||||||
|
RAMMB float64 `json:"ram_mb"`
|
||||||
|
}
|
||||||
|
|
||||||
func round2(v float64) float64 {
|
func round2(v float64) float64 {
|
||||||
return float64(int(v*100)) / 100
|
return float64(int(v*100)) / 100
|
||||||
}
|
}
|
||||||
@@ -110,11 +148,11 @@ func collectMetrics() (*Metricas, error) {
|
|||||||
m.Hostname = hn
|
m.Hostname = hn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uptime
|
// Uptime + OS
|
||||||
if info, err := host.Info(); err == nil {
|
if info, err := host.Info(); err == nil {
|
||||||
m.Uptime = info.Uptime
|
m.Uptime = info.Uptime
|
||||||
m.UptimeStr = uptimeStr(info.Uptime)
|
m.UptimeStr = uptimeStr(info.Uptime)
|
||||||
if info.OS != "" {
|
if info.Platform != "" {
|
||||||
m.OS = info.Platform + " " + info.PlatformVersion
|
m.OS = info.Platform + " " + info.PlatformVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,80 +168,149 @@ func collectMetrics() (*Metricas, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swap
|
||||||
|
if sv, err := mem.SwapMemory(); err == nil && sv.Total > 0 {
|
||||||
|
gb := 1024.0 * 1024 * 1024
|
||||||
|
m.Swap = SwapInfo{
|
||||||
|
TotalGB: round2(float64(sv.Total) / gb),
|
||||||
|
UsadoGB: round2(float64(sv.Used) / gb),
|
||||||
|
Porcentaje: round2(sv.UsedPercent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
m.CPU.Nucleos = runtime.NumCPU()
|
m.CPU.Nucleos = runtime.NumCPU()
|
||||||
if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 {
|
if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 {
|
||||||
m.CPU.Porcentaje = round2(pcts[0])
|
m.CPU.Porcentaje = round2(pcts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discos: todas las particiones físicas/reales
|
// Discos — incluir todo filesystem con espacio real (>100 MB), sin duplicar mountpoints
|
||||||
if parts, err := disk.Partitions(false); err == nil {
|
{
|
||||||
gb := 1024.0 * 1024 * 1024
|
gb := 1024.0 * 1024 * 1024
|
||||||
skipFSTypes := map[string]bool{
|
skipFS := map[string]bool{
|
||||||
"proc": true, "sysfs": true, "tmpfs": true, "devtmpfs": true,
|
"proc": true, "sysfs": true, "devtmpfs": true, "devpts": true,
|
||||||
"devpts": true, "cgroup": true, "cgroup2": true, "overlay": true,
|
"cgroup": true, "cgroup2": true, "hugetlbfs": true, "mqueue": true,
|
||||||
"hugetlbfs": true, "mqueue": true, "pstore": true, "securityfs": true,
|
"pstore": true, "securityfs": true, "debugfs": true, "tracefs": true,
|
||||||
"debugfs": true, "tracefs": true, "autofs": true, "fusectl": true,
|
"autofs": true, "fusectl": true, "configfs": true, "efivarfs": true,
|
||||||
"configfs": true, "efivarfs": true, "bpf": true, "rpc_pipefs": true,
|
"bpf": true, "rpc_pipefs": true, "nsfs": true, "ramfs": true,
|
||||||
"squashfs": true, "ramfs": true, "nsfs": true, "none": true,
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
parts, _ := disk.Partitions(true)
|
||||||
|
// Asegurar que siempre se incluye la raíz
|
||||||
|
type partEntry struct{ mountpoint, fstype string }
|
||||||
|
entries := []partEntry{{"/", ""}}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
entries = []partEntry{{"C:\\", ""}}
|
||||||
}
|
}
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
if skipFSTypes[p.Fstype] {
|
entries = append(entries, partEntry{p.Mountpoint, p.Fstype})
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if seen[e.mountpoint] || skipFS[e.fstype] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if d, err := disk.Usage(p.Mountpoint); err == nil {
|
d, err := disk.Usage(e.mountpoint)
|
||||||
m.Discos = append(m.Discos, DiscoInfo{
|
if err != nil || d.Total == 0 {
|
||||||
TotalGB: round2(float64(d.Total) / gb),
|
continue
|
||||||
UsadoGB: round2(float64(d.Used) / gb),
|
|
||||||
LibreGB: round2(float64(d.Free) / gb),
|
|
||||||
Porcentaje: round2(d.UsedPercent),
|
|
||||||
Ruta: p.Mountpoint,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
totalGB := float64(d.Total) / gb
|
||||||
}
|
if totalGB < 0.1 { // ignorar < 100 MB (tmpfs pequeños, etc.)
|
||||||
// Fallback: si no se encontraron particiones, usar raíz
|
continue
|
||||||
if len(m.Discos) == 0 {
|
}
|
||||||
root := "/"
|
seen[e.mountpoint] = true
|
||||||
if runtime.GOOS == "windows" {
|
m.Discos = append(m.Discos, DiscoInfo{
|
||||||
root = "C:\\"
|
TotalGB: round2(totalGB),
|
||||||
}
|
|
||||||
if d, err := disk.Usage(root); err == nil {
|
|
||||||
gb := 1024.0 * 1024 * 1024
|
|
||||||
m.Discos = []DiscoInfo{{
|
|
||||||
TotalGB: round2(float64(d.Total) / gb),
|
|
||||||
UsadoGB: round2(float64(d.Used) / gb),
|
UsadoGB: round2(float64(d.Used) / gb),
|
||||||
LibreGB: round2(float64(d.Free) / gb),
|
LibreGB: round2(float64(d.Free) / gb),
|
||||||
Porcentaje: round2(d.UsedPercent),
|
Porcentaje: round2(d.UsedPercent),
|
||||||
Ruta: root,
|
Ruta: e.mountpoint,
|
||||||
}}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load average (Linux/macOS)
|
// Load average
|
||||||
if la, err := load.Avg(); err == nil {
|
if la, err := load.Avg(); err == nil {
|
||||||
m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)}
|
m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Red (interfaces físicas, excluir loopback)
|
||||||
|
if counters, err := pnet.IOCounters(true); err == nil {
|
||||||
|
mb := 1024.0 * 1024
|
||||||
|
for _, c := range counters {
|
||||||
|
if c.Name == "lo" || c.Name == "lo0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.Red = append(m.Red, RedInfo{
|
||||||
|
Interface: c.Name,
|
||||||
|
BytesEnvMB: round2(float64(c.BytesSent) / mb),
|
||||||
|
BytesRecMB: round2(float64(c.BytesRecv) / mb),
|
||||||
|
PktEnv: c.PacketsSent,
|
||||||
|
PktRec: c.PacketsRecv,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCP conexiones activas
|
||||||
|
if conns, err := pnet.Connections("tcp"); err == nil {
|
||||||
|
m.TCPConns = len(conns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top 10 procesos: filtrar por RAM primero (barato), luego leer CPU solo en esos 10
|
||||||
|
if procs, err := process.Processes(); err == nil {
|
||||||
|
type pd struct {
|
||||||
|
proc *process.Process
|
||||||
|
name string
|
||||||
|
ramMB float64
|
||||||
|
}
|
||||||
|
var lista []pd
|
||||||
|
for _, p := range procs {
|
||||||
|
name, _ := p.Name()
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ramMB := 0.0
|
||||||
|
if mi, err := p.MemoryInfo(); err == nil && mi != nil {
|
||||||
|
ramMB = round2(float64(mi.RSS) / (1024 * 1024))
|
||||||
|
}
|
||||||
|
if ramMB < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lista = append(lista, pd{p, name, ramMB})
|
||||||
|
}
|
||||||
|
sort.Slice(lista, func(i, j int) bool {
|
||||||
|
return lista[i].ramMB > lista[j].ramMB
|
||||||
|
})
|
||||||
|
limit := 10
|
||||||
|
if len(lista) < limit {
|
||||||
|
limit = len(lista)
|
||||||
|
}
|
||||||
|
for _, d := range lista[:limit] {
|
||||||
|
cpuPct, _ := d.proc.CPUPercent() // solo 10 lecturas, no cientos
|
||||||
|
m.TopProcs = append(m.TopProcs, ProcesoInfo{d.proc.Pid, d.name, round2(cpuPct), d.ramMB})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Reporte ───────────────────────────────────────────────────────────────────
|
// ── Reporte ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type HeartbeatRequest struct {
|
type HeartbeatRequest struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Metricas string `json:"metricas"`
|
Metricas string `json:"metricas"`
|
||||||
|
ReniceAction string `json:"renice_action,omitempty"` // descripción si se reniceó algo
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendHeartbeat(cfg *Config, metricas *Metricas) error {
|
func sendHeartbeat(cfg *Config, metricas *Metricas, reniceAction string) error {
|
||||||
metJSON, err := json.Marshal(metricas)
|
metJSON, err := json.Marshal(metricas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := HeartbeatRequest{
|
payload := HeartbeatRequest{
|
||||||
Token: cfg.Token,
|
Token: cfg.Token,
|
||||||
Metricas: string(metJSON),
|
Metricas: string(metJSON),
|
||||||
|
ReniceAction: reniceAction,
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
|
|
||||||
@@ -238,14 +345,12 @@ func main() {
|
|||||||
|
|
||||||
cfg, err := loadConfig(*configPath)
|
cfg, err := loadConfig(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Si no hay config pero se pasaron flags, usarlos directamente
|
|
||||||
if *apiURL == "" || *token == "" {
|
if *apiURL == "" || *token == "" {
|
||||||
log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err)
|
log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err)
|
||||||
}
|
}
|
||||||
cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30}
|
cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flags sobreescriben config
|
|
||||||
if *apiURL != "" {
|
if *apiURL != "" {
|
||||||
cfg.APIURL = *apiURL
|
cfg.APIURL = *apiURL
|
||||||
}
|
}
|
||||||
@@ -257,12 +362,14 @@ func main() {
|
|||||||
log.Fatal("api_url y token son requeridos")
|
log.Fatal("api_url y token son requeridos")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds", cfg.APIURL, cfg.Interval)
|
log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds | Renice: %v (umbral %.0f%%, %d reportes)",
|
||||||
|
cfg.APIURL, cfg.Interval, cfg.ReniceEnabled, cfg.CPUThreshold, cfg.ReniceConsecutive)
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Enviar inmediatamente al iniciar
|
consecAlto := 0 // contador de reportes consecutivos con CPU alta
|
||||||
|
|
||||||
reportar := func() {
|
reportar := func() {
|
||||||
m, err := collectMetrics()
|
m, err := collectMetrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -273,11 +380,38 @@ func main() {
|
|||||||
b, _ := json.MarshalIndent(m, "", " ")
|
b, _ := json.MarshalIndent(m, "", " ")
|
||||||
log.Printf("📊 Métricas:\n%s", string(b))
|
log.Printf("📊 Métricas:\n%s", string(b))
|
||||||
}
|
}
|
||||||
if err := sendHeartbeat(cfg, m); err != nil {
|
|
||||||
|
// ── Lógica de renice ──────────────────────────────────────────────────
|
||||||
|
var reniceAction string
|
||||||
|
if cfg.ReniceEnabled && m.CPU.Porcentaje >= cfg.CPUThreshold {
|
||||||
|
consecAlto++
|
||||||
|
log.Printf("⚠️ CPU alta: %.1f%% (reporte %d/%d)", m.CPU.Porcentaje, consecAlto, cfg.ReniceConsecutive)
|
||||||
|
if consecAlto >= cfg.ReniceConsecutive && len(m.TopProcs) > 0 {
|
||||||
|
// Ordenar top_procs por CPU para encontrar el culpable real
|
||||||
|
procs := make([]ProcesoInfo, len(m.TopProcs))
|
||||||
|
copy(procs, m.TopProcs)
|
||||||
|
sort.Slice(procs, func(i, j int) bool { return procs[i].CPU > procs[j].CPU })
|
||||||
|
culpable := procs[0]
|
||||||
|
if culpable.CPU > 0 && culpable.PID > 0 {
|
||||||
|
cmd := exec.Command("renice", "-n", "19", "-p", fmt.Sprintf("%d", culpable.PID))
|
||||||
|
if rerr := cmd.Run(); rerr != nil {
|
||||||
|
log.Printf("❌ renice falló para PID %d (%s): %v", culpable.PID, culpable.Nombre, rerr)
|
||||||
|
} else {
|
||||||
|
reniceAction = fmt.Sprintf("renice +19 aplicado a %s (PID %d, CPU %.1f%%)", culpable.Nombre, culpable.PID, culpable.CPU)
|
||||||
|
log.Printf("🔧 %s", reniceAction)
|
||||||
|
consecAlto = 0 // reiniciar tras actuar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consecAlto = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sendHeartbeat(cfg, m, reniceAction); err != nil {
|
||||||
log.Printf("⚠️ Error enviando heartbeat: %v", err)
|
log.Printf("⚠️ Error enviando heartbeat: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("✅ Heartbeat enviado | RAM: %.1f%% | CPU: %.1f%% | Discos: %d",
|
log.Printf("✅ Heartbeat | RAM: %.1f%% | CPU: %.1f%% | TCP: %d | Procs: %d",
|
||||||
m.RAM.Porcentaje, m.CPU.Porcentaje, len(m.Discos))
|
m.RAM.Porcentaje, m.CPU.Porcentaje, m.TCPConns, len(m.TopProcs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ jwt:
|
|||||||
expire: 31536000
|
expire: 31536000
|
||||||
|
|
||||||
template:
|
template:
|
||||||
path: "resources/view"
|
path: "resources/views"
|
||||||
extension: ".html"
|
extension: ".html"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ func Migrate() {
|
|||||||
&models.VcardApiConfig{},
|
&models.VcardApiConfig{},
|
||||||
// Integración Coolify
|
// Integración Coolify
|
||||||
&models.CoolifyConfig{},
|
&models.CoolifyConfig{},
|
||||||
|
// Historial de métricas del agente
|
||||||
|
&models.ServidorMetricasHistory{},
|
||||||
|
// Monitor de disponibilidad de URLs
|
||||||
|
&models.UrlMonitor{},
|
||||||
|
&models.UrlMonitorLog{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("Error during main migration: %v", err)
|
log.Fatalf("Error during main migration: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,3 +103,49 @@ func DeleteServidor(servidor Servidor) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Historial de métricas ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ServidorMetricasHistory struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
ServidorID uint `json:"servidor_id" gorm:"column:servidor_id;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index"`
|
||||||
|
CpuPct float64 `json:"cpu_pct" gorm:"column:cpu_pct"`
|
||||||
|
RamPct float64 `json:"ram_pct" gorm:"column:ram_pct"`
|
||||||
|
SwapPct float64 `json:"swap_pct" gorm:"column:swap_pct"`
|
||||||
|
DiscoPct float64 `json:"disco_pct" gorm:"column:disco_pct"`
|
||||||
|
TCPConns int `json:"tcp_conns" gorm:"column:tcp_conns"`
|
||||||
|
Load1 float64 `json:"load1" gorm:"column:load1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ServidorMetricasHistory) TableName() string { return "servidor_metricas_history" }
|
||||||
|
|
||||||
|
func InsertMetricasHistory(servidorID uint, cpu, ram, swap, disco float64, tcp int, load1 float64) {
|
||||||
|
app.Http.Database.DB.Create(&ServidorMetricasHistory{
|
||||||
|
ServidorID: servidorID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
CpuPct: cpu,
|
||||||
|
RamPct: ram,
|
||||||
|
SwapPct: swap,
|
||||||
|
DiscoPct: disco,
|
||||||
|
TCPConns: tcp,
|
||||||
|
Load1: load1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMetricasHistory(servidorID uint, horas int) ([]ServidorMetricasHistory, error) {
|
||||||
|
var items []ServidorMetricasHistory
|
||||||
|
desde := time.Now().Add(-time.Duration(horas) * time.Hour)
|
||||||
|
err := app.Http.Database.DB.
|
||||||
|
Where("servidor_id = ? AND created_at >= ?", servidorID, desde).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurgarMetricasHistory(dias int) error {
|
||||||
|
corte := time.Now().AddDate(0, 0, -dias)
|
||||||
|
return app.Http.Database.DB.
|
||||||
|
Where("created_at < ?", corte).
|
||||||
|
Delete(&ServidorMetricasHistory{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UrlMonitor struct {
|
||||||
|
gorm.Model
|
||||||
|
Nombre string `json:"nombre" gorm:"column:nombre;size:100"`
|
||||||
|
URL string `json:"url" gorm:"column:url;size:500"`
|
||||||
|
IntervaloMin int `json:"intervalo_min" gorm:"column:intervalo_min;default:5"`
|
||||||
|
TimeoutSeg int `json:"timeout_seg" gorm:"column:timeout_seg;default:10"`
|
||||||
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
|
// Estado actual
|
||||||
|
UltimoStatus int `json:"ultimo_status" gorm:"column:ultimo_status"`
|
||||||
|
UltimaLatMs int64 `json:"ultima_lat_ms" gorm:"column:ultima_lat_ms"`
|
||||||
|
UltimoCheckAt *time.Time `json:"ultimo_check_at" gorm:"column:ultimo_check_at"`
|
||||||
|
UltimoOk *bool `json:"ultimo_ok" gorm:"column:ultimo_ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UrlMonitor) TableName() string { return "url_monitor" }
|
||||||
|
|
||||||
|
type UrlMonitorLog struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
MonitorID uint `json:"monitor_id" gorm:"column:monitor_id;index"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index"`
|
||||||
|
StatusCode int `json:"status_code" gorm:"column:status_code"`
|
||||||
|
LatenciaMs int64 `json:"latencia_ms" gorm:"column:latencia_ms"`
|
||||||
|
Ok bool `json:"ok" gorm:"column:ok"`
|
||||||
|
Error string `json:"error" gorm:"column:error;type:text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UrlMonitorLog) TableName() string { return "url_monitor_log" }
|
||||||
|
|
||||||
|
func GetAllUrlMonitors() ([]UrlMonitor, error) {
|
||||||
|
var items []UrlMonitor
|
||||||
|
err := app.Http.Database.DB.Order("nombre ASC").Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActiveUrlMonitors() ([]UrlMonitor, error) {
|
||||||
|
var items []UrlMonitor
|
||||||
|
err := app.Http.Database.DB.Where("activo = true").Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUrlMonitorByID(id uint) (*UrlMonitor, error) {
|
||||||
|
var m UrlMonitor
|
||||||
|
err := app.Http.Database.DB.First(&m, id).Error
|
||||||
|
return &m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUrlMonitor(m *UrlMonitor) error {
|
||||||
|
return app.Http.Database.DB.Create(m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveUrlMonitor(m *UrlMonitor) error {
|
||||||
|
return app.Http.Database.DB.Save(m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteUrlMonitor(id uint) error {
|
||||||
|
return app.Http.Database.DB.Delete(&UrlMonitor{}, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateUrlMonitorStatus(id uint, statusCode int, latMs int64, ok bool) error {
|
||||||
|
now := time.Now()
|
||||||
|
return app.Http.Database.DB.Model(&UrlMonitor{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
|
"ultimo_status": statusCode,
|
||||||
|
"ultima_lat_ms": latMs,
|
||||||
|
"ultimo_check_at": &now,
|
||||||
|
"ultimo_ok": ok,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertUrlMonitorLog(monitorID uint, statusCode int, latMs int64, ok bool, errStr string) {
|
||||||
|
app.Http.Database.DB.Create(&UrlMonitorLog{
|
||||||
|
MonitorID: monitorID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
StatusCode: statusCode,
|
||||||
|
LatenciaMs: latMs,
|
||||||
|
Ok: ok,
|
||||||
|
Error: errStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUrlMonitorLogs(monitorID uint, horas int) ([]UrlMonitorLog, error) {
|
||||||
|
var items []UrlMonitorLog
|
||||||
|
desde := time.Now().Add(-time.Duration(horas) * time.Hour)
|
||||||
|
err := app.Http.Database.DB.
|
||||||
|
Where("monitor_id = ? AND created_at >= ?", monitorID, desde).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&items).Error
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurgarUrlMonitorLogs(dias int) error {
|
||||||
|
corte := time.Now().AddDate(0, 0, -dias)
|
||||||
|
return app.Http.Database.DB.Where("created_at < ?", corte).Delete(&UrlMonitorLog{}).Error
|
||||||
|
}
|
||||||
+169
-10
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +13,12 @@ import (
|
|||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cronProcInfo struct {
|
||||||
|
Nombre string `json:"nombre"`
|
||||||
|
CPU float64 `json:"cpu"`
|
||||||
|
RAMMB float64 `json:"ram_mb"`
|
||||||
|
}
|
||||||
|
|
||||||
var cronScheduler *cron.Cron
|
var cronScheduler *cron.Cron
|
||||||
|
|
||||||
// IniciarCron arranca el scheduler de tareas. Llamar desde app.go o main.go.
|
// IniciarCron arranca el scheduler de tareas. Llamar desde app.go o main.go.
|
||||||
@@ -41,6 +49,27 @@ func IniciarCron() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purga de historial de métricas — cada noche a las 3 AM (retención 7 días)
|
||||||
|
if _, err := cronScheduler.AddFunc("0 3 * * *", func() {
|
||||||
|
if err := models.PurgarMetricasHistory(7); err != nil {
|
||||||
|
log.Printf("[CRON] Error purgando historial métricas: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("[CRON] Historial de métricas purgado (>7 días)")
|
||||||
|
}
|
||||||
|
if err := models.PurgarUrlMonitorLogs(7); err != nil {
|
||||||
|
log.Printf("[CRON] Error purgando logs URL monitor: %v", err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("[CRON] Error registrando tarea purga_metricas: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor de URLs — cada minuto
|
||||||
|
if _, err := cronScheduler.AddFunc("* * * * *", VerificarUrlMonitors); err != nil {
|
||||||
|
log.Printf("[CRON] Error registrando tarea url_monitor: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cronScheduler.Start()
|
cronScheduler.Start()
|
||||||
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min")
|
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min")
|
||||||
}
|
}
|
||||||
@@ -251,14 +280,24 @@ func VerificarSaludServidores() {
|
|||||||
// 2. Recursos (CPU / RAM / Disco) al límite
|
// 2. Recursos (CPU / RAM / Disco) al límite
|
||||||
if srv.MetricasJson != "" {
|
if srv.MetricasJson != "" {
|
||||||
var m struct {
|
var m struct {
|
||||||
CPU struct{ Porcentaje float64 `json:"porcentaje"` } `json:"cpu"`
|
CPU struct{ Porcentaje float64 `json:"porcentaje"` } `json:"cpu"`
|
||||||
RAM struct{ Porcentaje float64 `json:"porcentaje"` } `json:"ram"`
|
RAM struct{ Porcentaje float64 `json:"porcentaje"` } `json:"ram"`
|
||||||
Disco struct{ Porcentaje float64 `json:"porcentaje"` } `json:"disco"`
|
Discos []struct {
|
||||||
|
Porcentaje float64 `json:"porcentaje"`
|
||||||
|
Ruta string `json:"ruta"`
|
||||||
|
} `json:"discos"`
|
||||||
|
TopProcs []cronProcInfo `json:"top_procs"`
|
||||||
}
|
}
|
||||||
if jsonErr := json.Unmarshal([]byte(srv.MetricasJson), &m); jsonErr == nil {
|
if jsonErr := json.Unmarshal([]byte(srv.MetricasJson), &m); jsonErr == nil {
|
||||||
checkRecurso(srv.Nombre, "CPU", m.CPU.Porcentaje, cfg.UmbralCPU)
|
checkRecurso(srv.Nombre, "CPU", m.CPU.Porcentaje, cfg.UmbralCPU, m.TopProcs, "cpu")
|
||||||
checkRecurso(srv.Nombre, "RAM", m.RAM.Porcentaje, cfg.UmbralRAM)
|
checkRecurso(srv.Nombre, "RAM", m.RAM.Porcentaje, cfg.UmbralRAM, m.TopProcs, "ram")
|
||||||
checkRecurso(srv.Nombre, "Disco", m.Disco.Porcentaje, cfg.UmbralDisco)
|
for _, d := range m.Discos {
|
||||||
|
label := "Disco"
|
||||||
|
if d.Ruta != "" && d.Ruta != "/" {
|
||||||
|
label = "Disco " + d.Ruta
|
||||||
|
}
|
||||||
|
checkRecurso(srv.Nombre, label, d.Porcentaje, cfg.UmbralDisco, nil, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,15 +322,34 @@ func VerificarSaludServidores() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRecurso(nombre, recurso string, valor float64, umbral int) {
|
func checkRecurso(nombre, recurso string, valor float64, umbral int, procs []cronProcInfo, ordenar string) {
|
||||||
if valor < float64(umbral) {
|
if valor < float64(umbral) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
titulo := fmt.Sprintf("⚠️ %s alto en %s (%.0f%%)", recurso, nombre, valor)
|
titulo := fmt.Sprintf("⚠️ %s alto en %s (%.0f%%)", recurso, nombre, valor)
|
||||||
if !models.YaExisteAlertaServidor(titulo, 1) {
|
if !models.YaExisteAlertaServidor(titulo, 1) {
|
||||||
crearNotifServidor(titulo,
|
cuerpo := fmt.Sprintf("%s al %.0f%% (umbral: %d%%)", recurso, valor, umbral)
|
||||||
fmt.Sprintf("%s al %.0f%% (umbral: %d%%)", recurso, valor, umbral),
|
if len(procs) > 0 {
|
||||||
"servidor_recurso_alto")
|
cp := make([]cronProcInfo, len(procs))
|
||||||
|
copy(cp, procs)
|
||||||
|
if ordenar == "cpu" {
|
||||||
|
sort.Slice(cp, func(i, j int) bool { return cp[i].CPU > cp[j].CPU })
|
||||||
|
} else {
|
||||||
|
sort.Slice(cp, func(i, j int) bool { return cp[i].RAMMB > cp[j].RAMMB })
|
||||||
|
}
|
||||||
|
if len(cp) > 3 {
|
||||||
|
cp = cp[:3]
|
||||||
|
}
|
||||||
|
cuerpo += "\n\nTop procesos:"
|
||||||
|
for i, p := range cp {
|
||||||
|
if ordenar == "cpu" {
|
||||||
|
cuerpo += fmt.Sprintf("\n%d. %s — CPU: %.1f%%, RAM: %.0f MB", i+1, p.Nombre, p.CPU, p.RAMMB)
|
||||||
|
} else {
|
||||||
|
cuerpo += fmt.Sprintf("\n%d. %s — RAM: %.0f MB, CPU: %.1f%%", i+1, p.Nombre, p.RAMMB, p.CPU)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crearNotifServidor(titulo, cuerpo, "servidor_recurso_alto")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,3 +372,104 @@ func crearNotifServidor(titulo, cuerpo, evento string) {
|
|||||||
sendTelegramAdmin(msg)
|
sendTelegramAdmin(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Monitor de URLs ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func VerificarUrlMonitors() {
|
||||||
|
monitors, err := models.GetActiveUrlMonitors()
|
||||||
|
if err != nil || len(monitors) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
for _, m := range monitors {
|
||||||
|
m := m
|
||||||
|
if m.UltimoCheckAt != nil {
|
||||||
|
if now.Sub(*m.UltimoCheckAt).Minutes() < float64(m.IntervaloMin) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go ejecutarChequeoURL(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EjecutarChequeoURLPublic(m models.UrlMonitor) { ejecutarChequeoURL(m) }
|
||||||
|
|
||||||
|
func ejecutarChequeoURL(m models.UrlMonitor) {
|
||||||
|
statusCode, latMs, err := doHttpCheck(m.URL, m.TimeoutSeg)
|
||||||
|
ok := err == nil && statusCode >= 200 && statusCode < 400
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
anteriorOk := m.UltimoOk == nil || *m.UltimoOk
|
||||||
|
|
||||||
|
models.UpdateUrlMonitorStatus(m.ID, statusCode, latMs, ok)
|
||||||
|
models.InsertUrlMonitorLog(m.ID, statusCode, latMs, ok, errStr)
|
||||||
|
|
||||||
|
if m.UltimoOk == nil {
|
||||||
|
return // primer chequeo, no alertar
|
||||||
|
}
|
||||||
|
if !ok && anteriorOk {
|
||||||
|
detalle := errStr
|
||||||
|
if detalle == "" {
|
||||||
|
detalle = fmt.Sprintf("HTTP %d", statusCode)
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("🔴 <b>Sitio caído</b>\n<b>%s</b>\n%s\n\nError: %s",
|
||||||
|
escapeTelegramHTML(m.Nombre),
|
||||||
|
escapeTelegramHTML(m.URL),
|
||||||
|
escapeTelegramHTML(detalle))
|
||||||
|
sendTelegramAdmin(msg)
|
||||||
|
log.Printf("[URL-MON] DOWN %s — %s", m.Nombre, detalle)
|
||||||
|
} else if ok && !anteriorOk {
|
||||||
|
msg := fmt.Sprintf("✅ <b>Sitio recuperado</b>\n<b>%s</b>\n%s\nLatencia: <b>%dms</b>",
|
||||||
|
escapeTelegramHTML(m.Nombre),
|
||||||
|
escapeTelegramHTML(m.URL),
|
||||||
|
latMs)
|
||||||
|
sendTelegramAdmin(msg)
|
||||||
|
log.Printf("[URL-MON] UP %s — %dms", m.Nombre, latMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHttpCheck(url string, timeoutSeg int) (int, int64, error) {
|
||||||
|
if timeoutSeg <= 0 {
|
||||||
|
timeoutSeg = 10
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Duration(timeoutSeg) * time.Second,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= 5 {
|
||||||
|
return fmt.Errorf("demasiadas redirecciones")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "usite-monitor/1.0")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
latMs := time.Since(start).Milliseconds()
|
||||||
|
if err != nil || (resp != nil && resp.StatusCode == http.StatusMethodNotAllowed) {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
// Reintentar con GET
|
||||||
|
start = time.Now()
|
||||||
|
req2, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
req2.Header.Set("User-Agent", "usite-monitor/1.0")
|
||||||
|
resp2, err2 := client.Do(req2)
|
||||||
|
latMs = time.Since(start).Milliseconds()
|
||||||
|
if err2 != nil {
|
||||||
|
return 0, latMs, err2
|
||||||
|
}
|
||||||
|
resp2.Body.Close()
|
||||||
|
return resp2.StatusCode, latMs, nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp.StatusCode, latMs, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -246,6 +246,14 @@ func sendTelegramPortalUser(chatID string, mensaje string) error {
|
|||||||
return fmt.Errorf("no hay bots de Telegram activos para enviar")
|
return fmt.Errorf("no hay bots de Telegram activos para enviar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotificarReniceAction envía Telegram cuando el agente aplica un renice automático.
|
||||||
|
func NotificarReniceAction(servidorNombre, accion string) {
|
||||||
|
msg := fmt.Sprintf("🔧 <b>Renice automático aplicado</b>\nServidor: <b>%s</b>\n%s\n\n<i>CPU sostenida por encima del umbral.</i>",
|
||||||
|
escapeTelegramHTML(servidorNombre),
|
||||||
|
escapeTelegramHTML(accion))
|
||||||
|
sendTelegramAdmin(msg)
|
||||||
|
}
|
||||||
|
|
||||||
func sendTelegramAdmin(mensaje string) {
|
func sendTelegramAdmin(mensaje string) {
|
||||||
configs, err := models.GetAllTelegramConfigs()
|
configs, err := models.GetAllTelegramConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ type WebSmsResponse struct {
|
|||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
SubID string `json:"subid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WebSmsResponse) MsgID() string {
|
||||||
|
if r.ID != "" {
|
||||||
|
return r.ID
|
||||||
|
}
|
||||||
|
return r.SubID
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebSmsAckPayload struct {
|
type WebSmsAckPayload struct {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<!-- Servidor Dashboard -->
|
<!-- Servidor Dashboard -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||||||
<div x-data="servidorDashboard()" x-init="init()">
|
<div x-data="servidorDashboard()" x-init="init()">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-8 flex items-center justify-between">
|
<div class="mb-8 flex items-center justify-between">
|
||||||
@@ -109,6 +110,79 @@
|
|||||||
<p class="text-[10px] text-slate-400 mt-0.5" x-text="'+' + (m.discos.length - 3) + ' más'"></p>
|
<p class="text-[10px] text-slate-400 mt-0.5" x-text="'+' + (m.discos.length - 3) + ' más'"></p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Swap -->
|
||||||
|
<template x-if="m.swap?.total_gb > 0">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-slate-500 font-medium">🔄 Swap</span>
|
||||||
|
<span class="font-mono font-bold text-slate-700"
|
||||||
|
x-text="m.swap.usado_gb + ' / ' + m.swap.total_gb + ' GB (' + m.swap.porcentaje + '%)'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 rounded-full h-1.5">
|
||||||
|
<div class="h-1.5 rounded-full transition-all"
|
||||||
|
:class="m.swap.porcentaje > 85 ? 'bg-red-500' : m.swap.porcentaje > 50 ? 'bg-amber-400' : 'bg-purple-400'"
|
||||||
|
:style="'width:' + Math.min(m.swap.porcentaje, 100) + '%'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Red -->
|
||||||
|
<template x-if="(m.red || []).length > 0">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-slate-500 font-medium">🌐 Red</span>
|
||||||
|
<span class="text-[10px] text-slate-400" x-text="(m.red || []).length + ' interfaz(es)'"></span>
|
||||||
|
</div>
|
||||||
|
<template x-for="r in (m.red || []).slice(0,2)" :key="r.interface">
|
||||||
|
<div class="flex justify-between text-[10px] text-slate-500 font-mono mb-0.5">
|
||||||
|
<span x-text="r.interface"></span>
|
||||||
|
<span>
|
||||||
|
<span class="text-green-600">↓ <span x-text="fmtMB(r.bytes_rec_mb)"></span></span>
|
||||||
|
<span class="text-blue-600 ml-1">↑ <span x-text="fmtMB(r.bytes_env_mb)"></span></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- TCP + Top Procs -->
|
||||||
|
<div class="flex items-center justify-between text-xs text-slate-500 pt-1 border-t border-slate-100">
|
||||||
|
<span>🔌 TCP: <strong x-text="m.tcp_conns ?? '—'"></strong></span>
|
||||||
|
<span>📊 Load: <strong x-text="m.load_avg?.load1 ?? '—'"></strong></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Procesos -->
|
||||||
|
<template x-if="(m.top_procs || []).length > 0">
|
||||||
|
<div x-data="{ showProcs: false }">
|
||||||
|
<button @click="showProcs=!showProcs" class="w-full text-left text-[10px] text-slate-400 hover:text-slate-600 flex items-center gap-1">
|
||||||
|
<svg class="w-3 h-3 transition-transform" :class="showProcs ? 'rotate-90' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||||
|
Top procesos por RAM (<span x-text="(m.top_procs||[]).length"></span>)
|
||||||
|
</button>
|
||||||
|
<template x-if="showProcs">
|
||||||
|
<div class="mt-1.5 bg-slate-50 rounded-lg overflow-hidden">
|
||||||
|
<table class="w-full text-[10px]">
|
||||||
|
<thead><tr class="text-slate-400 border-b border-slate-200">
|
||||||
|
<th class="px-2 py-1 text-left">Proceso</th>
|
||||||
|
<th class="px-2 py-1 text-right">CPU%</th>
|
||||||
|
<th class="px-2 py-1 text-right">RAM</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="p in (m.top_procs||[]).slice(0,10)" :key="p.pid">
|
||||||
|
<tr class="border-b border-slate-100 last:border-0">
|
||||||
|
<td class="px-2 py-1 font-mono truncate max-w-[90px]" x-text="p.nombre"></td>
|
||||||
|
<td class="px-2 py-1 text-right font-bold"
|
||||||
|
:class="p.cpu > 50 ? 'text-red-600' : p.cpu > 20 ? 'text-amber-600' : 'text-slate-600'"
|
||||||
|
x-text="p.cpu + '%'"></td>
|
||||||
|
<td class="px-2 py-1 text-right text-slate-500 font-mono" x-text="fmtMB(p.ram_mb)"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Uptime + OS -->
|
<!-- Uptime + OS -->
|
||||||
<div class="flex justify-between text-xs text-slate-400 pt-1 border-t border-slate-100">
|
<div class="flex justify-between text-xs text-slate-400 pt-1 border-t border-slate-100">
|
||||||
<span>⏱ <span x-text="m.uptime || '—'"></span></span>
|
<span>⏱ <span x-text="m.uptime || '—'"></span></span>
|
||||||
@@ -282,6 +356,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- === Historial de Métricas === -->
|
||||||
|
<template x-if="servidorSeleccionado?.agent_token">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider">Historial de Métricas</h3>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<template x-for="opt in [{h:1,l:'1h'},{h:6,l:'6h'},{h:24,l:'24h'},{h:168,l:'7d'}]" :key="opt.h">
|
||||||
|
<button @click="cargarHistorial(opt.h)"
|
||||||
|
:class="historialHoras === opt.h ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'"
|
||||||
|
class="px-3 py-1 rounded-lg text-xs font-bold transition-colors"
|
||||||
|
x-text="opt.l"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||||
|
<template x-if="historialCargando">
|
||||||
|
<div class="flex items-center justify-center h-40 text-slate-400 text-sm gap-2">
|
||||||
|
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||||
|
Cargando historial...
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!historialCargando && historialVacio">
|
||||||
|
<div class="flex items-center justify-center h-40 text-slate-400 text-sm">Sin datos en este período</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="!historialCargando && !historialVacio">
|
||||||
|
<canvas id="metricasChart" height="120"></canvas>
|
||||||
|
</div>
|
||||||
|
<!-- Leyenda -->
|
||||||
|
<div x-show="!historialCargando && !historialVacio" class="flex gap-4 mt-3 justify-center text-xs text-slate-500">
|
||||||
|
<span class="flex items-center gap-1.5"><span class="w-3 h-1 rounded bg-blue-500 inline-block"></span>CPU %</span>
|
||||||
|
<span class="flex items-center gap-1.5"><span class="w-3 h-1 rounded bg-green-500 inline-block"></span>RAM %</span>
|
||||||
|
<span class="flex items-center gap-1.5"><span class="w-3 h-1 rounded bg-amber-400 inline-block"></span>Disco %</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- === Conexiones BD === -->
|
<!-- === Conexiones BD === -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">
|
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">
|
||||||
@@ -528,6 +639,10 @@
|
|||||||
syncCargando: {},
|
syncCargando: {},
|
||||||
pingCargando: {},
|
pingCargando: {},
|
||||||
pingResultado: {},
|
pingResultado: {},
|
||||||
|
historialHoras: 24,
|
||||||
|
historialCargando: false,
|
||||||
|
historialVacio: false,
|
||||||
|
_chart: null,
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.cargarServidores();
|
await this.cargarServidores();
|
||||||
@@ -551,12 +666,13 @@
|
|||||||
this.servidorSeleccionado = { ...servidor };
|
this.servidorSeleccionado = { ...servidor };
|
||||||
this.pingCargando = {};
|
this.pingCargando = {};
|
||||||
this.pingResultado = {};
|
this.pingResultado = {};
|
||||||
|
this.historialHoras = 24;
|
||||||
|
this.historialVacio = false;
|
||||||
this.cargandoConexiones = true;
|
this.cargandoConexiones = true;
|
||||||
|
if (this._chart) { this._chart.destroy(); this._chart = null; }
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/app/servidor-dashboard/${servidor.ID}`);
|
const r = await fetch(`/app/servidor-dashboard/${servidor.ID}`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
// Inyectar estado de ping directamente en cada conexión para que
|
|
||||||
// Alpine v3 lo rastreé correctamente dentro del x-for
|
|
||||||
const conexiones = (data.conexiones || []).map(c => ({
|
const conexiones = (data.conexiones || []).map(c => ({
|
||||||
...c,
|
...c,
|
||||||
_pingCargando: false,
|
_pingCargando: false,
|
||||||
@@ -571,12 +687,112 @@
|
|||||||
} finally {
|
} finally {
|
||||||
this.cargandoConexiones = false;
|
this.cargandoConexiones = false;
|
||||||
}
|
}
|
||||||
|
if (servidor.agent_token) {
|
||||||
|
await this.$nextTick();
|
||||||
|
this.cargarHistorial(24);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cerrarModal() {
|
cerrarModal() {
|
||||||
|
if (this._chart) { this._chart.destroy(); this._chart = null; }
|
||||||
this.servidorSeleccionado = null;
|
this.servidorSeleccionado = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async cargarHistorial(horas) {
|
||||||
|
if (!this.servidorSeleccionado) return;
|
||||||
|
this.historialHoras = horas;
|
||||||
|
this.historialCargando = true;
|
||||||
|
this.historialVacio = false;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/app/servidor/${this.servidorSeleccionado.ID}/metricas-history?horas=${horas}`);
|
||||||
|
const data = await r.json();
|
||||||
|
if (!data || data.length === 0) { this.historialVacio = true; return; }
|
||||||
|
// Downsample si hay muchos puntos (>300 para fluidez)
|
||||||
|
const pts = data.length > 300
|
||||||
|
? data.filter((_, i) => i % Math.ceil(data.length / 300) === 0)
|
||||||
|
: data;
|
||||||
|
const labels = pts.map(p => {
|
||||||
|
const d = new Date(p.created_at);
|
||||||
|
return horas <= 6
|
||||||
|
? d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit', second:'2-digit'})
|
||||||
|
: horas <= 24
|
||||||
|
? d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit'})
|
||||||
|
: d.toLocaleDateString('es', {month:'short', day:'numeric'}) + ' ' + d.toLocaleTimeString('es', {hour:'2-digit', minute:'2-digit'});
|
||||||
|
});
|
||||||
|
await this.$nextTick();
|
||||||
|
const canvas = document.getElementById('metricasChart');
|
||||||
|
if (!canvas) return;
|
||||||
|
if (this._chart) this._chart.destroy();
|
||||||
|
this._chart = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'CPU %',
|
||||||
|
data: pts.map(p => p.cpu_pct),
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59,130,246,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'RAM %',
|
||||||
|
data: pts.map(p => p.ram_pct),
|
||||||
|
borderColor: '#22c55e',
|
||||||
|
backgroundColor: 'rgba(34,197,94,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Disco %',
|
||||||
|
data: pts.map(p => p.disco_pct),
|
||||||
|
borderColor: '#f59e0b',
|
||||||
|
backgroundColor: 'rgba(245,158,11,0.06)',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.1,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
interaction: { mode: 'index', intersect: false },
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: { maxTicksLimit: 8, font: { size: 10 }, color: '#94a3b8' },
|
||||||
|
grid: { color: '#f1f5f9' },
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: 0, max: 100,
|
||||||
|
ticks: { stepSize: 25, font: { size: 10 }, color: '#94a3b8',
|
||||||
|
callback: v => v + '%' },
|
||||||
|
grid: { color: '#f1f5f9' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error cargando historial:', e);
|
||||||
|
this.historialVacio = true;
|
||||||
|
} finally {
|
||||||
|
this.historialCargando = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
abrirAgentModal(servidor) {
|
abrirAgentModal(servidor) {
|
||||||
// Buscar el servidor actualizado en la lista
|
// Buscar el servidor actualizado en la lista
|
||||||
const s = this.servidores.find(sv => sv.ID === servidor.ID) || servidor;
|
const s = this.servidores.find(sv => sv.ID === servidor.ID) || servidor;
|
||||||
@@ -654,6 +870,13 @@
|
|||||||
catch { return {}; }
|
catch { return {}; }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fmtMB(mb) {
|
||||||
|
if (mb === undefined || mb === null) return '—';
|
||||||
|
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
|
||||||
|
if (mb >= 1) return mb.toFixed(0) + ' MB';
|
||||||
|
return (mb * 1024).toFixed(0) + ' KB';
|
||||||
|
},
|
||||||
|
|
||||||
copiar(texto) {
|
copiar(texto) {
|
||||||
navigator.clipboard.writeText(texto).catch(() => {});
|
navigator.clipboard.writeText(texto).catch(() => {});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -111,6 +111,111 @@
|
|||||||
<p x-show="copied" x-text="copied" class="text-green-600 text-xs mt-3"></p>
|
<p x-show="copied" x-text="copied" class="text-green-600 text-xs mt-3"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Documentación de integración -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6" x-data="{ tab: 'curl' }">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold text-slate-800">Documentación de integración</h2>
|
||||||
|
<p class="text-sm text-slate-500 mt-0.5">Cómo enviar SMS desde tu propio sistema</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-1 bg-green-100 text-green-700 rounded-full font-bold">REST API</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Endpoint -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Endpoint</p>
|
||||||
|
<div class="flex items-center gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2">
|
||||||
|
<span class="text-xs font-bold bg-blue-600 text-white px-2 py-0.5 rounded font-mono">POST</span>
|
||||||
|
<code class="text-sm font-mono text-slate-700 flex-1">https://admin.u-site.app/api/sms/send</code>
|
||||||
|
<button type="button" @click="copy('https://admin.u-site.app/api/sms/send')" class="btn-icon text-slate-400 hover:text-slate-600" title="Copiar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Autenticación</p>
|
||||||
|
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-800">
|
||||||
|
Incluye tu API Key en el header <code class="bg-amber-100 px-1 rounded font-mono text-xs">Authorization: Bearer {API_KEY}</code>.<br>
|
||||||
|
Encuentra tu API Key en la sección <strong>API Key (envío externo)</strong> de esta página.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Request/Response -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Body (JSON)</p>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-4">
|
||||||
|
<pre class="text-green-400 text-xs font-mono leading-relaxed">{
|
||||||
|
"numero": "573001234567",
|
||||||
|
"mensaje": "Hola, este es tu código: 9821"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 space-y-1">
|
||||||
|
<div class="flex gap-2 text-xs"><code class="text-blue-600 font-mono font-bold w-16">numero</code><span class="text-slate-500">Teléfono en formato internacional sin <code class="bg-slate-100 px-1 rounded">+</code> (ej: <code class="bg-slate-100 px-1 rounded">573001234567</code>)</span></div>
|
||||||
|
<div class="flex gap-2 text-xs"><code class="text-blue-600 font-mono font-bold w-16">mensaje</code><span class="text-slate-500">Texto del SMS. Máx 160 caracteres para un crédito.</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Respuestas</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-bold text-green-700">200 — Enviado</span>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-3 mt-1">
|
||||||
|
<pre class="text-green-400 text-xs font-mono">{ "ok": true, "id": "6a3d4f155..." }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-bold text-red-600">401 — No autenticado</span>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-3 mt-1">
|
||||||
|
<pre class="text-red-400 text-xs font-mono">{ "error": "token requerido" }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-bold text-red-600">400 — Datos inválidos</span>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-3 mt-1">
|
||||||
|
<pre class="text-red-400 text-xs font-mono">{ "error": "numero y mensaje son requeridos" }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-bold text-red-600">500 — Error LabsMobile</span>
|
||||||
|
<div class="bg-slate-900 rounded-lg p-3 mt-1">
|
||||||
|
<pre class="text-red-400 text-xs font-mono">{ "error": "descripción del error" }</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code examples -->
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Ejemplos de código</p>
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex gap-1 mb-3 border-b border-slate-200">
|
||||||
|
<template x-for="t in [{id:'curl',label:'cURL'},{id:'js',label:'JavaScript'},{id:'php',label:'PHP'},{id:'python',label:'Python'}]" :key="t.id">
|
||||||
|
<button type="button" @click="tab=t.id"
|
||||||
|
:class="tab===t.id ? 'border-b-2 border-green-600 text-green-700 font-bold' : 'text-slate-500 hover:text-slate-700'"
|
||||||
|
class="px-3 py-1.5 text-xs transition-colors -mb-px"
|
||||||
|
x-text="t.label"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bloque de código único — el contenido lo genera JS -->
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="bg-slate-900 rounded-lg p-4">
|
||||||
|
<pre class="text-green-400 text-xs font-mono leading-relaxed whitespace-pre-wrap" x-text="getCodeExample(tab)"></pre>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="copy(getCodeExample(tab))"
|
||||||
|
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-slate-700 hover:bg-slate-600 text-slate-300 rounded px-2 py-1 text-xs">
|
||||||
|
Copiar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p x-show="copied" x-text="copied" class="text-green-600 text-xs mt-3"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
<h2 class="text-lg font-bold text-slate-800 mb-4">Últimos envíos</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Últimos envíos</h2>
|
||||||
@@ -196,7 +301,83 @@ function websmsApp() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); },
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); },
|
||||||
copy(text){ navigator.clipboard.writeText(text); this.copied='URL copiada'; setTimeout(()=>this.copied='', 2000); },
|
copy(text){ navigator.clipboard.writeText(text); this.copied='Copiado'; setTimeout(()=>this.copied='', 2000); },
|
||||||
|
|
||||||
|
getCodeExample(tab) {
|
||||||
|
const key = this.secretKey || '{API_KEY}';
|
||||||
|
const url = 'https://admin.u-site.app/api/sms/send';
|
||||||
|
if (tab === 'curl') {
|
||||||
|
return [
|
||||||
|
"curl -X POST " + url + " \\",
|
||||||
|
" -H 'Authorization: Bearer " + key + "' \\",
|
||||||
|
" -H 'Content-Type: application/json' \\",
|
||||||
|
" -d '{",
|
||||||
|
' "numero": "573001234567",',
|
||||||
|
' "mensaje": "Hola, este es tu código: 9821"',
|
||||||
|
" }'"
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
if (tab === 'js') {
|
||||||
|
return [
|
||||||
|
"const response = await fetch('" + url + "', {",
|
||||||
|
" method: 'POST',",
|
||||||
|
" headers: {",
|
||||||
|
" 'Authorization': 'Bearer " + key + "',",
|
||||||
|
" 'Content-Type': 'application/json',",
|
||||||
|
" },",
|
||||||
|
" body: JSON.stringify({",
|
||||||
|
" numero: '573001234567',",
|
||||||
|
" mensaje: 'Hola, este es tu código: 9821',",
|
||||||
|
" }),",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"const data = await response.json();",
|
||||||
|
"console.log(data); // { ok: true, id: '6a3d...' }"
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
if (tab === 'php') {
|
||||||
|
return [
|
||||||
|
"<?php",
|
||||||
|
"$ch = curl_init('" + url + "');",
|
||||||
|
"curl_setopt_array($ch, [",
|
||||||
|
" CURLOPT_RETURNTRANSFER => true,",
|
||||||
|
" CURLOPT_POST => true,",
|
||||||
|
" CURLOPT_HTTPHEADER => [",
|
||||||
|
" 'Authorization: Bearer " + key + "',",
|
||||||
|
" 'Content-Type: application/json',",
|
||||||
|
" ],",
|
||||||
|
' CURLOPT_POSTFIELDS => json_encode([',
|
||||||
|
" 'numero' => '573001234567',",
|
||||||
|
" 'mensaje' => 'Hola, este es tu código: 9821',",
|
||||||
|
" ]),",
|
||||||
|
"]);",
|
||||||
|
"$response = json_decode(curl_exec($ch), true);",
|
||||||
|
"curl_close($ch);",
|
||||||
|
"// $response['ok'] === true"
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
if (tab === 'python') {
|
||||||
|
return [
|
||||||
|
"import requests",
|
||||||
|
"",
|
||||||
|
"response = requests.post(",
|
||||||
|
" '" + url + "',",
|
||||||
|
" headers={",
|
||||||
|
" 'Authorization': 'Bearer " + key + "',",
|
||||||
|
" 'Content-Type': 'application/json',",
|
||||||
|
" },",
|
||||||
|
" json={",
|
||||||
|
" 'numero': '573001234567',",
|
||||||
|
" 'mensaje': 'Hola, este es tu código: 9821',",
|
||||||
|
" },",
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
"data = response.json()",
|
||||||
|
"print(data) # {'ok': True, 'id': '6a3d...'}"
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentHeartbeat recibe métricas del agente instalado en un servidor.
|
// AgentHeartbeat recibe métricas del agente instalado en un servidor.
|
||||||
// Ruta pública: POST /agent/heartbeat — autenticada por token en JSON body.
|
// Ruta pública: POST /agent/heartbeat — autenticada por token en JSON body.
|
||||||
func AgentHeartbeat(c *fiber.Ctx) error {
|
func AgentHeartbeat(c *fiber.Ctx) error {
|
||||||
type HeartbeatRequest struct {
|
type HeartbeatRequest struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Metricas string `json:"metricas"` // JSON string ya serializado por el agente
|
Metricas string `json:"metricas"`
|
||||||
|
ReniceAction string `json:"renice_action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var req HeartbeatRequest
|
var req HeartbeatRequest
|
||||||
@@ -89,9 +91,80 @@ func AgentHeartbeat(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": "no se pudo guardar"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": "no se pudo guardar"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insertar punto en el historial de métricas
|
||||||
|
if req.Metricas != "" {
|
||||||
|
var raw map[string]any
|
||||||
|
if json.Unmarshal([]byte(req.Metricas), &raw) == nil {
|
||||||
|
getFloat := func(obj map[string]any, key string) float64 {
|
||||||
|
if sub, ok := obj[key].(map[string]any); ok {
|
||||||
|
if v, ok := sub["porcentaje"].(float64); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
discoPct := 0.0
|
||||||
|
if discos, ok := raw["discos"].([]any); ok && len(discos) > 0 {
|
||||||
|
if d, ok := discos[0].(map[string]any); ok {
|
||||||
|
if v, ok := d["porcentaje"].(float64); ok {
|
||||||
|
discoPct = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tcpConns := 0
|
||||||
|
if v, ok := raw["tcp_conns"].(float64); ok {
|
||||||
|
tcpConns = int(v)
|
||||||
|
}
|
||||||
|
load1 := 0.0
|
||||||
|
if la, ok := raw["load_avg"].(map[string]any); ok {
|
||||||
|
if v, ok := la["load1"].(float64); ok {
|
||||||
|
load1 = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swapPct := 0.0
|
||||||
|
if sw, ok := raw["swap"].(map[string]any); ok {
|
||||||
|
if v, ok := sw["porcentaje"].(float64); ok {
|
||||||
|
swapPct = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
models.InsertMetricasHistory(servidor.ID,
|
||||||
|
getFloat(raw, "cpu"),
|
||||||
|
getFloat(raw, "ram"),
|
||||||
|
swapPct,
|
||||||
|
discoPct,
|
||||||
|
tcpConns,
|
||||||
|
load1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notificar por Telegram si el agente aplicó un renice
|
||||||
|
if req.ReniceAction != "" {
|
||||||
|
go services.NotificarReniceAction(servidor.Nombre, req.ReniceAction)
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID})
|
return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMetricasHistory devuelve el historial de métricas de un servidor.
|
||||||
|
// GET /app/servidor/:id/metricas-history?horas=24
|
||||||
|
func GetMetricasHistory(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
horas := c.QueryInt("horas", 24)
|
||||||
|
if horas <= 0 || horas > 168 {
|
||||||
|
horas = 24
|
||||||
|
}
|
||||||
|
var srv models.Servidor
|
||||||
|
if err := app.Http.Database.DB.First(&srv, id).Error; err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"})
|
||||||
|
}
|
||||||
|
items, err := models.GetMetricasHistory(srv.ID, horas)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(items)
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateAgentToken genera o regenera el token de agente para un servidor.
|
// GenerateAgentToken genera o regenera el token de agente para un servidor.
|
||||||
// Ruta protegida: POST /app/servidor/:id/agent-token
|
// Ruta protegida: POST /app/servidor/:id/agent-token
|
||||||
func GenerateAgentToken(c *fiber.Ctx) error {
|
func GenerateAgentToken(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UrlMonitorIndex(c *fiber.Ctx) error {
|
||||||
|
return c.Render("url_monitor", fiber.Map{
|
||||||
|
"user": c.Locals("user"),
|
||||||
|
"modules": c.Locals("userModules"),
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUrlMonitors(c *fiber.Ctx) error {
|
||||||
|
items, err := models.GetAllUrlMonitors()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUrlMonitor(c *fiber.Ctx) error {
|
||||||
|
var m models.UrlMonitor
|
||||||
|
if err := c.BodyParser(&m); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(m.Nombre) == "" || strings.TrimSpace(m.URL) == "" {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "nombre y url son requeridos"})
|
||||||
|
}
|
||||||
|
if m.IntervaloMin <= 0 {
|
||||||
|
m.IntervaloMin = 5
|
||||||
|
}
|
||||||
|
if m.TimeoutSeg <= 0 {
|
||||||
|
m.TimeoutSeg = 10
|
||||||
|
}
|
||||||
|
m.Activo = true
|
||||||
|
if err := models.CreateUrlMonitor(&m); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateUrlMonitor(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
existing, err := models.GetUrlMonitorByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(404).JSON(fiber.Map{"error": "no encontrado"})
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(existing); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
existing.ID = uint(id)
|
||||||
|
if err := models.SaveUrlMonitor(existing); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteUrlMonitorHandler(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
if err := models.DeleteUrlMonitor(uint(id)); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckUrlMonitorNow(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
m, err := models.GetUrlMonitorByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(404).JSON(fiber.Map{"error": "no encontrado"})
|
||||||
|
}
|
||||||
|
go services.EjecutarChequeoURLPublic(*m)
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "message": "Chequeo iniciado"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUrlMonitorLogsHandler(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
horas := c.QueryInt("horas", 24)
|
||||||
|
if horas <= 0 || horas > 168 {
|
||||||
|
horas = 24
|
||||||
|
}
|
||||||
|
items, err := models.GetUrlMonitorLogs(uint(id), horas)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(items)
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ func TestWebSms(c *fiber.Ctx) error {
|
|||||||
Para: b.Para,
|
Para: b.Para,
|
||||||
Mensaje: b.Mensaje,
|
Mensaje: b.Mensaje,
|
||||||
Status: resp.Code,
|
Status: resp.Code,
|
||||||
MsgID: resp.ID,
|
MsgID: resp.MsgID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return c.JSON(fiber.Map{"ok": true, "response": resp})
|
return c.JSON(fiber.Map{"ok": true, "response": resp})
|
||||||
@@ -189,14 +189,14 @@ func ApiSendSms(c *fiber.Ctx) error {
|
|||||||
Para: b.Numero,
|
Para: b.Numero,
|
||||||
Mensaje: b.Mensaje,
|
Mensaje: b.Mensaje,
|
||||||
Status: status,
|
Status: status,
|
||||||
MsgID: func() string { if resp != nil { return resp.ID }; return "" }(),
|
MsgID: func() string { if resp != nil { return resp.MsgID() }; return "" }(),
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(500).JSON(fiber.Map{"error": errStr})
|
return c.Status(500).JSON(fiber.Map{"error": errStr})
|
||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{"ok": true, "id": resp.ID})
|
return c.JSON(fiber.Map{"ok": true, "id": resp.MsgID()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func WebSmsIncomingWebhook(c *fiber.Ctx) error {
|
func WebSmsIncomingWebhook(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ func AuthAdmin(c *fiber.Ctx) error {
|
|||||||
func AuthApi() func(*fiber.Ctx) error {
|
func AuthApi() func(*fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
// Excluir rutas públicas
|
// Excluir rutas públicas
|
||||||
if c.Path() == "/api/v1/oauth/token" {
|
if c.Path() == "/api/v1/oauth/token" || c.Path() == "/api/sms/send" {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,8 +81,18 @@ func UserRoutes(app fiber.Router) {
|
|||||||
protected.Get("/servidor-dashboard/:id", controllers.GetServidorDashboard)
|
protected.Get("/servidor-dashboard/:id", controllers.GetServidorDashboard)
|
||||||
protected.Get("/conx-ping/:id", controllers.PingConexion)
|
protected.Get("/conx-ping/:id", controllers.PingConexion)
|
||||||
protected.Post("/servidor/:id/agent-token", controllers.GenerateAgentToken)
|
protected.Post("/servidor/:id/agent-token", controllers.GenerateAgentToken)
|
||||||
|
protected.Get("/servidor/:id/metricas-history", controllers.GetMetricasHistory)
|
||||||
protected.Post("/servidor/:id/sync-hostinger", controllers.SyncServidorFromHostinger)
|
protected.Post("/servidor/:id/sync-hostinger", controllers.SyncServidorFromHostinger)
|
||||||
|
|
||||||
|
// Monitor de URLs
|
||||||
|
protected.Get("/url-monitor", middlewares.MenuMiddleware, controllers.UrlMonitorIndex)
|
||||||
|
protected.Get("/url-monitors", controllers.GetUrlMonitors)
|
||||||
|
protected.Post("/url-monitor", controllers.CreateUrlMonitor)
|
||||||
|
protected.Put("/url-monitor/:id", controllers.UpdateUrlMonitor)
|
||||||
|
protected.Delete("/url-monitor/:id", controllers.DeleteUrlMonitorHandler)
|
||||||
|
protected.Post("/url-monitor/:id/check", controllers.CheckUrlMonitorNow)
|
||||||
|
protected.Get("/url-monitor/:id/logs", controllers.GetUrlMonitorLogsHandler)
|
||||||
|
|
||||||
// Rutas de proveedores de servidor
|
// Rutas de proveedores de servidor
|
||||||
protected.Get("/prov_servidor", middlewares.MenuMiddleware, controllers.ProvServidor)
|
protected.Get("/prov_servidor", middlewares.MenuMiddleware, controllers.ProvServidor)
|
||||||
protected.Get("/loadprovservidor", controllers.GetProvServidor)
|
protected.Get("/loadprovservidor", controllers.GetProvServidor)
|
||||||
|
|||||||
Reference in New Issue
Block a user