fix: ping real de BD + campo Host en ConxDb + reactividad Alpine
Problema raíz:
1. El ping TCP usaba Servidor.IpServidor → fallaba si la BD solo escucha
en localhost del servidor remoto (comportamiento normal en producción)
2. La reactividad Alpine v3.13 no detectaba keys nuevas en objeto vacío {}
dentro de x-for loops, spinner nunca aparecía
Cambios:
- pkg/models/conx_db.go: nuevo campo Host (vacío = usa Servidor.IpServidor)
+ método HostEfectivo() como fuente única de verdad para host
- pkg/services/query_runner_service.go: todos los puntos (openDynamicDB,
openDynamicDBWithName, redisConnect, mongoURI) usan c.HostEfectivo()
- rest/controllers/servidor_controller.go: PingConexion reescrito —
primero prueba conexión real a la BD (no solo TCP), si falla prueba TCP,
devuelve host+puerto testeado y mensajes de error descriptivos
- servidor_dashboard.html: hacerPing usa spread-replace para forzar
reactividad + $nextTick, resultado muestra host:puerto testeado y
mensaje de error completo en múltiples líneas
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
c3f19991c9
commit
2779c0a15d
@@ -10,6 +10,7 @@ import (
|
|||||||
type ConxDb struct {
|
type ConxDb struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Nombre string `json:"nombre" gorm:"column:nombre"`
|
Nombre string `json:"nombre" gorm:"column:nombre"`
|
||||||
|
Host string `json:"host" gorm:"column:host"` // vacío = usa Servidor.IpServidor
|
||||||
Puerto string `json:"puerto" gorm:"column:puerto"`
|
Puerto string `json:"puerto" gorm:"column:puerto"`
|
||||||
Usuario string `json:"usuario" gorm:"column:usuario"`
|
Usuario string `json:"usuario" gorm:"column:usuario"`
|
||||||
Password string `json:"password" gorm:"column:password"`
|
Password string `json:"password" gorm:"column:password"`
|
||||||
@@ -21,6 +22,15 @@ type ConxDb struct {
|
|||||||
Servidor Servidor `json:"servidor" gorm:"foreignKey:ServidorID"`
|
Servidor Servidor `json:"servidor" gorm:"foreignKey:ServidorID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostEfectivo devuelve el host de conexión: el campo Host si está definido,
|
||||||
|
// o el IP del servidor asociado como fallback.
|
||||||
|
func (c ConxDb) HostEfectivo() string {
|
||||||
|
if c.Host != "" {
|
||||||
|
return c.Host
|
||||||
|
}
|
||||||
|
return c.Servidor.IpServidor
|
||||||
|
}
|
||||||
|
|
||||||
func (ConxDb) TableName() string {
|
func (ConxDb) TableName() string {
|
||||||
return "conx_db"
|
return "conx_db"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type QueryResult struct {
|
|||||||
// openDynamicDB abre una conexión a la base de datos indicada por ConxDb.
|
// openDynamicDB abre una conexión a la base de datos indicada por ConxDb.
|
||||||
func openDynamicDB(c models.ConxDb) (*sql.DB, error) {
|
func openDynamicDB(c models.ConxDb) (*sql.DB, error) {
|
||||||
driver := strings.ToLower(c.TipoDb.Nombre)
|
driver := strings.ToLower(c.TipoDb.Nombre)
|
||||||
host := c.Servidor.IpServidor
|
host := c.HostEfectivo()
|
||||||
port := c.Puerto
|
port := c.Puerto
|
||||||
user := c.Usuario
|
user := c.Usuario
|
||||||
pass := c.Password
|
pass := c.Password
|
||||||
@@ -319,7 +319,7 @@ func TestDBConnection(conx models.ConxDb) error {
|
|||||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) {
|
func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) {
|
||||||
host := c.Servidor.IpServidor
|
host := c.HostEfectivo()
|
||||||
port := c.Puerto
|
port := c.Puerto
|
||||||
user := c.Usuario
|
user := c.Usuario
|
||||||
pass := c.Password
|
pass := c.Password
|
||||||
@@ -486,7 +486,7 @@ func isRedisDriver(driver string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func redisConnect(c models.ConxDb, dbIndex int) (*goredis.Client, error) {
|
func redisConnect(c models.ConxDb, dbIndex int) (*goredis.Client, error) {
|
||||||
host := c.Servidor.IpServidor
|
host := c.HostEfectivo()
|
||||||
port := c.Puerto
|
port := c.Puerto
|
||||||
pass := c.Password
|
pass := c.Password
|
||||||
if port == "" {
|
if port == "" {
|
||||||
@@ -637,10 +637,10 @@ func redisExecuteCommand(conx models.ConxDb, database, cmdText string) QueryResu
|
|||||||
}
|
}
|
||||||
elapsed := time.Since(start).Milliseconds()
|
elapsed := time.Since(start).Milliseconds()
|
||||||
result := QueryResult{
|
result := QueryResult{
|
||||||
Columns: []string{"command", "result", "error"},
|
Columns: []string{"command", "result", "error"},
|
||||||
Rows: rows,
|
Rows: rows,
|
||||||
RowCount: len(rows),
|
RowCount: len(rows),
|
||||||
IsSelect: true,
|
IsSelect: true,
|
||||||
DurationMs: elapsed,
|
DurationMs: elapsed,
|
||||||
}
|
}
|
||||||
saveHistory(conx.ID, cmdText, "ok", "", int64(len(rows)), elapsed)
|
saveHistory(conx.ID, cmdText, "ok", "", int64(len(rows)), elapsed)
|
||||||
@@ -700,11 +700,11 @@ func redisResultToQueryResult(val any, cmd string) QueryResult {
|
|||||||
label = "count"
|
label = "count"
|
||||||
}
|
}
|
||||||
return QueryResult{
|
return QueryResult{
|
||||||
IsSelect: false,
|
IsSelect: false,
|
||||||
AffectedRows: v,
|
AffectedRows: v,
|
||||||
Columns: []string{label},
|
Columns: []string{label},
|
||||||
Rows: []map[string]any{{label: v}},
|
Rows: []map[string]any{{label: v}},
|
||||||
RowCount: 1,
|
RowCount: 1,
|
||||||
}
|
}
|
||||||
case []any:
|
case []any:
|
||||||
// Lista o conjunto de valores
|
// Lista o conjunto de valores
|
||||||
@@ -797,7 +797,7 @@ func isMongoDriver(driver string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mongoURI(c models.ConxDb) string {
|
func mongoURI(c models.ConxDb) string {
|
||||||
host := c.Servidor.IpServidor
|
host := c.HostEfectivo()
|
||||||
port := c.Puerto
|
port := c.Puerto
|
||||||
user := c.Usuario
|
user := c.Usuario
|
||||||
pass := c.Password
|
pass := c.Password
|
||||||
|
|||||||
@@ -288,10 +288,16 @@
|
|||||||
<!-- Resultado del ping -->
|
<!-- Resultado del ping -->
|
||||||
<div x-show="pingResultado[cx.ID || cx.id] !== undefined && pingResultado[cx.ID || cx.id] !== null" class="mt-1">
|
<div x-show="pingResultado[cx.ID || cx.id] !== undefined && pingResultado[cx.ID || cx.id] !== null" class="mt-1">
|
||||||
<div :class="pingResultado[cx.ID || cx.id]?.exitoso ? 'bg-green-50 border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800'"
|
<div :class="pingResultado[cx.ID || cx.id]?.exitoso ? 'bg-green-50 border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800'"
|
||||||
class="rounded-lg px-3 py-2 border text-xs flex items-center gap-2">
|
class="rounded-lg px-3 py-2 border text-xs flex flex-col gap-1">
|
||||||
<span x-text="pingResultado[cx.ID || cx.id]?.exitoso ? '✓' : '✗'" class="font-bold text-base leading-none"></span>
|
<div class="flex items-center gap-2">
|
||||||
<span x-show="pingResultado[cx.ID || cx.id]?.exitoso" x-text="'Conectado en ' + pingResultado[cx.ID || cx.id]?.tiempo + ' ms'"></span>
|
<span x-text="pingResultado[cx.ID || cx.id]?.exitoso ? '✓' : '✗'" class="font-bold text-base leading-none"></span>
|
||||||
<span x-show="!pingResultado[cx.ID || cx.id]?.exitoso" x-text="pingResultado[cx.ID || cx.id]?.error || 'Sin conexión'"></span>
|
<span x-show="pingResultado[cx.ID || cx.id]?.exitoso" x-text="'Conectado en ' + pingResultado[cx.ID || cx.id]?.tiempo + ' ms'"></span>
|
||||||
|
<span x-show="!pingResultado[cx.ID || cx.id]?.exitoso" class="font-medium">Sin conexión</span>
|
||||||
|
</div>
|
||||||
|
<span x-show="pingResultado[cx.ID || cx.id]?.host" class="font-mono opacity-70"
|
||||||
|
x-text="(pingResultado[cx.ID || cx.id]?.host || '') + ':' + (pingResultado[cx.ID || cx.id]?.puerto || '')"></span>
|
||||||
|
<span x-show="!pingResultado[cx.ID || cx.id]?.exitoso && pingResultado[cx.ID || cx.id]?.error"
|
||||||
|
x-text="pingResultado[cx.ID || cx.id]?.error" class="opacity-80 break-words"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -575,8 +581,10 @@
|
|||||||
async hacerPing(cx) {
|
async hacerPing(cx) {
|
||||||
const id = cx.ID || cx.id;
|
const id = cx.ID || cx.id;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
this.pingCargando[id] = true;
|
// Spread-replace para forzar reactividad Alpine v3 con keys nuevas
|
||||||
this.pingResultado[id] = undefined;
|
this.pingCargando = { ...this.pingCargando, [id]: true };
|
||||||
|
this.pingResultado = { ...this.pingResultado, [id]: null };
|
||||||
|
await this.$nextTick();
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/app/conx-ping/${id}`);
|
const r = await fetch(`/app/conx-ping/${id}`);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -584,11 +592,11 @@
|
|||||||
throw new Error(`HTTP ${r.status}${txt ? ': ' + txt.substring(0, 80) : ''}`);
|
throw new Error(`HTTP ${r.status}${txt ? ': ' + txt.substring(0, 80) : ''}`);
|
||||||
}
|
}
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
this.pingResultado[id] = data;
|
this.pingResultado = { ...this.pingResultado, [id]: data };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.pingResultado[id] = { exitoso: false, error: e.message || 'Sin conexión' };
|
this.pingResultado = { ...this.pingResultado, [id]: { exitoso: false, error: e.message || 'Sin conexión' } };
|
||||||
} finally {
|
} finally {
|
||||||
this.pingCargando[id] = false;
|
this.pingCargando = { ...this.pingCargando, [id]: false };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ func PingConexion(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtener conexión de BD
|
|
||||||
var conexion models.ConxDb
|
var conexion models.ConxDb
|
||||||
db := app.Http.Database.DB
|
db := app.Http.Database.DB
|
||||||
if err := db.Preload("TipoDb").Preload("Servidor").First(&conexion, uid).Error; err != nil {
|
if err := db.Preload("TipoDb").Preload("Servidor").First(&conexion, uid).Error; err != nil {
|
||||||
@@ -245,41 +244,46 @@ func PingConexion(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realizar ping a la conexión
|
host := conexion.HostEfectivo()
|
||||||
|
puerto := conexion.Puerto
|
||||||
|
|
||||||
|
// Primero intentar conexión real a la BD (más fiable que solo TCP)
|
||||||
inicio := time.Now()
|
inicio := time.Now()
|
||||||
exitoso := false
|
testErr := services.TestDBConnection(conexion)
|
||||||
var tiempoMs int64
|
if testErr == nil {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"exitoso": true,
|
||||||
|
"tiempo": time.Since(inicio).Milliseconds(),
|
||||||
|
"host": host,
|
||||||
|
"puerto": puerto,
|
||||||
|
"tipo": conexion.TipoDb.Nombre,
|
||||||
|
"error": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Intentar conexión TCP
|
// Si la BD no conecta, probar al menos TCP (puerto abierto)
|
||||||
addr := fmt.Sprintf("%s:%s", conexion.Servidor.IpServidor, conexion.Puerto)
|
tcpAddr := fmt.Sprintf("%s:%s", host, puerto)
|
||||||
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
inicioTCP := time.Now()
|
||||||
if err == nil {
|
conn, errTCP := net.DialTimeout("tcp", tcpAddr, 3*time.Second)
|
||||||
|
if errTCP == nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
exitoso = true
|
return c.JSON(fiber.Map{
|
||||||
tiempoMs = time.Since(inicio).Milliseconds()
|
"exitoso": false,
|
||||||
}
|
"tiempo": time.Since(inicioTCP).Milliseconds(),
|
||||||
|
"host": host,
|
||||||
// Si falla TCP, intentar con la BD específica
|
"puerto": puerto,
|
||||||
if !exitoso {
|
"tipo": conexion.TipoDb.Nombre,
|
||||||
inicio = time.Now()
|
// Puerto abierto pero la BD rechaza la conexión
|
||||||
testErr := services.TestDBConnection(conexion)
|
"error": fmt.Sprintf("Puerto %s abierto pero la BD no responde: %s", puerto, testErr.Error()),
|
||||||
if testErr == nil {
|
})
|
||||||
exitoso = true
|
|
||||||
tiempoMs = time.Since(inicio).Milliseconds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMsg := ""
|
|
||||||
if !exitoso {
|
|
||||||
errorMsg = fmt.Sprintf("No se pudo conectar a %s:%s", conexion.Servidor.IpServidor, conexion.Puerto)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"exitoso": exitoso,
|
"exitoso": false,
|
||||||
"tiempo": tiempoMs,
|
"tiempo": int64(0),
|
||||||
"host": conexion.Servidor.IpServidor,
|
"host": host,
|
||||||
"puerto": conexion.Puerto,
|
"puerto": puerto,
|
||||||
"tipo": conexion.TipoDb.Nombre,
|
"tipo": conexion.TipoDb.Nombre,
|
||||||
"error": errorMsg,
|
"error": fmt.Sprintf("No se puede alcanzar %s:%s — verifica que el host sea accesible desde este servidor. Tip: si el DB solo escucha en localhost del servidor, configura el campo 'Host' de la conexión.", host, puerto),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user