Fix Discord channel activation system

- Fix MySQL boolean conversion in toggle_channel_status
- Improve cache management with 5-second timeout
- Add bulk channel selection and toggle functionality
- Fix Jinja2 template syntax errors
- Add comprehensive debugging for channel status queries
- Implement real-time channel activation without container restart
This commit is contained in:
2026-03-20 06:41:35 -06:00
parent 100fef5c90
commit 39f531a331
6 changed files with 743 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
import os
import sys
import signal
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import discord
@@ -9,7 +11,7 @@ import re
import html
from botdiscord.config import get_discord_token, load_config, get_languages
from botdiscord.database import init_db, get_active_languages, get_bot_languages, save_message, get_welcome_message
from botdiscord.database import init_db, get_active_languages, get_bot_languages, save_message, get_welcome_message, is_channel_enabled
from botdiscord.ui import PersistentTranslationView, ConfigView, WelcomeTranslationView, TranslationButton
from botdiscord.translate import get_reverse_mapping, load_lang_mappings, get_name_to_code, get_flag_mapping
@@ -115,6 +117,14 @@ def get_active_langs_for_guild(guild_id):
async def on_message(message):
if message.author.bot: return
# Verificar si el canal está habilitado para traducción
channel_enabled = is_channel_enabled(message.channel.id)
print(f"[Bot] Mensaje en canal {message.channel.id} ({message.channel.name}) - Habilitado: {channel_enabled} - Timestamp: {time.strftime('%H:%M:%S')}")
if not channel_enabled:
print(f"[Bot] Ignorando mensaje en canal desactivado: {message.channel.name}")
return
text_content = message.content.strip()
if not text_content: return

View File

@@ -190,6 +190,19 @@ def init_db():
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
cursor.execute('''CREATE TABLE IF NOT EXISTS discord_servers
(server_id BIGINT NOT NULL PRIMARY KEY,
server_name VARCHAR(255) NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
cursor.execute('''CREATE TABLE IF NOT EXISTS discord_channels
(channel_id BIGINT NOT NULL PRIMARY KEY,
channel_name VARCHAR(255) NOT NULL,
server_id BIGINT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES discord_servers(server_id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''')
conn.commit()
cursor.close()
else:
@@ -243,6 +256,20 @@ def init_db():
channel_id INTEGER NOT NULL,
message_content TEXT NOT NULL,
enabled INTEGER DEFAULT 1)''')
c.execute('''CREATE TABLE IF NOT EXISTS discord_servers
(server_id INTEGER PRIMARY KEY,
server_name TEXT NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS discord_channels
(channel_id INTEGER PRIMARY KEY,
channel_name TEXT NOT NULL,
server_id INTEGER NOT NULL,
is_active INTEGER DEFAULT 1,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES discord_servers(server_id))''')
conn.commit()
conn.close()
@@ -716,3 +743,232 @@ def delete_welcome_message(guild_id: int):
c.execute("DELETE FROM welcome_messages WHERE guild_id = ?", (guild_id,))
conn.commit()
conn.close()
# Funciones para gestión de servidores y canales de Discord
def sync_discord_servers(servers: list):
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
for server in servers:
server_id = server.get('id')
server_name = server.get('name', '')
cursor.execute("""INSERT INTO discord_servers (server_id, server_name)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE server_name = %s""",
(server_id, server_name, server_name))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
for server in servers:
server_id = server.get('id')
server_name = server.get('name', '')
c.execute("""INSERT OR REPLACE INTO discord_servers (server_id, server_name)
VALUES (?, ?)""", (server_id, server_name))
conn.commit()
conn.close()
def get_discord_servers() -> list:
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT server_id, server_name, added_at FROM discord_servers ORDER BY server_name")
rows = cursor.fetchall()
cursor.close()
return rows
else:
conn = get_connection()
c = conn.cursor()
c.execute("SELECT server_id, server_name, added_at FROM discord_servers ORDER BY server_name")
rows = [{"server_id": r[0], "server_name": r[1], "added_at": r[2]} for r in c.fetchall()]
conn.close()
return rows
def sync_discord_channels(server_id: int, channels: list):
db_type = get_db_type()
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
for channel in channels:
channel_id = channel.get('id')
channel_name = channel.get('name', '')
cursor.execute("""INSERT INTO discord_channels (channel_id, channel_name, server_id)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE channel_name = %s, server_id = %s""",
(channel_id, channel_name, server_id, channel_name, server_id))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
for channel in channels:
channel_id = channel.get('id')
channel_name = channel.get('name', '')
c.execute("""INSERT OR REPLACE INTO discord_channels (channel_id, channel_name, server_id)
VALUES (?, ?, ?)""", (channel_id, channel_name, server_id))
conn.commit()
conn.close()
# Limpiar todo el cache de canales después de sincronizar
clear_channel_cache()
print(f"[DB] Canales sincronizados para servidor {server_id}, cache limpiado")
except Exception as e:
print(f"[DB] Error sincronizando canales del servidor {server_id}: {e}")
raise
def get_discord_channels(server_id: int = None) -> list:
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor(dictionary=True)
if server_id:
cursor.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels WHERE server_id = %s ORDER BY channel_name", (server_id,))
else:
cursor.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels ORDER BY server_id, channel_name")
rows = cursor.fetchall()
cursor.close()
return rows
else:
conn = get_connection()
c = conn.cursor()
if server_id:
c.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels WHERE server_id = ? ORDER BY channel_name", (server_id,))
else:
c.execute("SELECT channel_id, channel_name, server_id, is_active, added_at FROM discord_channels ORDER BY server_id, channel_name")
rows = [{"channel_id": r[0], "channel_name": r[1], "server_id": r[2], "is_active": bool(r[3]), "added_at": r[4]} for r in c.fetchall()]
conn.close()
return rows
def toggle_channel_status(channel_id: int, is_active: bool):
db_type = get_db_type()
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("UPDATE discord_channels SET is_active = %s WHERE channel_id = %s",
(1 if is_active else 0, channel_id))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("UPDATE discord_channels SET is_active = ? WHERE channel_id = ?",
(1 if is_active else 0, channel_id))
conn.commit()
conn.close()
# Limpiar el cache para forzar re-lectura
clear_channel_cache(channel_id)
print(f"[DB] Canal {channel_id} actualizado a {is_active}, cache limpiado")
except Exception as e:
print(f"[DB] Error actualizando canal {channel_id}: {e}")
raise
# Cache simple para estados de canales (evita consultas excesivas)
_channel_status_cache = {}
_cache_timeout = 5 # segundos
_last_cache_update = {}
def is_channel_enabled(channel_id: int) -> bool:
global _channel_status_cache, _last_cache_update
db_type = get_db_type()
current_time = time.time()
# Verificar si tenemos un cache válido
if (channel_id in _channel_status_cache and
channel_id in _last_cache_update and
current_time - _last_cache_update[channel_id] < _cache_timeout):
result = _channel_status_cache[channel_id]
print(f"[DB Cache] Canal {channel_id} estado (cache): {result}")
return result
try:
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT is_active FROM discord_channels WHERE channel_id = %s", (channel_id,))
row = cursor.fetchone()
cursor.close()
print(f"[DB] MySQL Query: SELECT is_active FROM discord_channels WHERE channel_id = {channel_id}")
print(f"[DB] MySQL Row: {row}")
result = bool(row[0]) if row else False
else:
conn = get_connection()
c = conn.cursor()
c.execute("SELECT is_active FROM discord_channels WHERE channel_id = ?", (channel_id,))
row = c.fetchone()
conn.close()
print(f"[DB] SQLite Query: SELECT is_active FROM discord_channels WHERE channel_id = {channel_id}")
print(f"[DB] SQLite Row: {row}")
result = bool(row[0]) if row else False
# Actualizar cache
_channel_status_cache[channel_id] = result
_last_cache_update[channel_id] = current_time
print(f"[DB] Canal {channel_id} estado (DB): {result} - Timestamp: {time.strftime('%H:%M:%S')}")
return result
except Exception as e:
print(f"[DB] Error verificando canal {channel_id}: {e}")
# En caso de error, devolver False por seguridad
return False
def clear_channel_cache(channel_id: int = None):
"""Limpia el cache de un canal específico o de todos"""
global _channel_status_cache, _last_cache_update
if channel_id:
_channel_status_cache.pop(channel_id, None)
_last_cache_update.pop(channel_id, None)
print(f"[DB] Cache limpiado para canal {channel_id}")
# Forzar expiración inmediata poniendo un timestamp antiguo
_last_cache_update[channel_id] = 0
else:
_channel_status_cache.clear()
_last_cache_update.clear()
print(f"[DB] Todo el cache de canales limpiado")
def delete_server_channels(server_id: int):
"""Elimina todos los canales de un servidor específico"""
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM discord_channels WHERE server_id = %s", (server_id,))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM discord_channels WHERE server_id = ?", (server_id,))
conn.commit()
conn.close()
def delete_discord_server(server_id: int):
"""Elimina un servidor y todos sus canales"""
db_type = get_db_type()
if db_type == "mysql":
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM discord_servers WHERE server_id = %s", (server_id,))
conn.commit()
cursor.close()
else:
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM discord_servers WHERE server_id = ?", (server_id,))
conn.commit()
conn.close()

View File

@@ -8,9 +8,14 @@ from fastapi.templating import Jinja2Templates
from fastapi import Request
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from dotenv import load_dotenv
from passlib.hash import pbkdf2_sha256 as hasher
from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type
# Asegurar que las variables de entorno se carguen correctamente
load_dotenv()
load_config() # Cargamos configuración inmediatamente
from botdiscord.database import (
@@ -397,6 +402,188 @@ async def delete_welcome(request: Request):
return RedirectResponse(url="/welcome", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/discord-channels")
async def discord_channels_page(request: Request):
if request.cookies.get("auth") != "ok":
return RedirectResponse(url="/login")
from botdiscord.database import get_discord_servers, get_discord_channels
servers = get_discord_servers()
channels = get_discord_channels()
# Agrupar canales por servidor
servers_with_channels = {}
for server in servers:
server_id = server['server_id']
servers_with_channels[server_id] = {
'server_info': server,
'channels': [ch for ch in channels if ch['server_id'] == server_id]
}
success = request.query_params.get("success") == "1"
error = request.query_params.get("error") == "1"
synced = request.query_params.get("synced") == "1"
return templates.TemplateResponse("discord_channels.html", {
"request": request,
"servers_with_channels": servers_with_channels,
"success": success,
"error": error,
"synced": synced
})
@app.post("/discord-channels/sync")
async def sync_discord_servers(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
try:
print("[Panel] Iniciando sincronización de Discord...")
# Diagnóstico de variables de entorno
print("[Panel] Variables de entorno disponibles:")
discord_env_vars = {k: v for k, v in os.environ.items() if 'DISCORD' in k or 'TOKEN' in k}
for key, value in discord_env_vars.items():
print(f" {key}: {'***' if 'TOKEN' in key else value}")
# Importar y usar el módulo de sincronización
from botdiscord.server_sync import sync_discord_servers_from_api
result = await sync_discord_servers_from_api()
print(f"[Panel] Resultado de sincronización: {result}")
if result:
return RedirectResponse(url="/discord-channels?synced=1", status_code=status.HTTP_303_SEE_OTHER)
else:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error en sincronización: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/toggle")
async def toggle_discord_channel(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
channel_id = form.get("channel_id")
is_active = form.get("is_active") == "true"
if not channel_id:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import toggle_channel_status
toggle_channel_status(int(channel_id), is_active)
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"Error toggling channel: {e}")
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/delete-server")
async def delete_discord_server(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
server_id = form.get("server_id")
if not server_id:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import delete_discord_server
delete_discord_server(int(server_id))
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"Error deleting server: {e}")
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/reload-bot")
async def reload_discord_bot(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
try:
print("[Panel] Enviando señal de recarga al bot de Discord...")
# Importar y ejecutar la función de recarga
from botdiscord.bot_reload import reload_bot_config
result = await reload_bot_config()
if result:
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
else:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error recargando bot: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/discord-channels/bulk-toggle")
async def bulk_toggle_discord_channels(request: Request):
if request.cookies.get("auth") != "ok":
raise HTTPException(status_code=401)
form = await request.form()
channel_ids = form.getlist("channel_ids")
bulk_action = form.get("bulk_action")
if not channel_ids or not bulk_action:
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
try:
from botdiscord.database import toggle_channel_status
is_active = (bulk_action == "activate")
success_count = 0
for channel_id_str in channel_ids:
try:
channel_id = int(channel_id_str)
toggle_channel_status(channel_id, is_active)
success_count += 1
except ValueError:
continue
print(f"[Panel] Bulk toggle: {success_count} canales {'activados' if is_active else 'desactivados'}")
return RedirectResponse(url="/discord-channels?success=1", status_code=status.HTTP_303_SEE_OTHER)
except Exception as e:
print(f"[Panel] Error en bulk toggle: {e}")
import traceback
traceback.print_exc()
return RedirectResponse(url="/discord-channels?error=1", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/diagnosis")
async def diagnosis_page(request: Request):
if request.cookies.get("auth") != "ok":
return RedirectResponse(url="/login")
# Obtener variables de entorno relevantes
env_vars = {
'DISCORD_TOKEN': os.getenv('DISCORD_TOKEN'),
'TELEGRAM_TOKEN': os.getenv('TELEGRAM_TOKEN'),
'LIBRETRANSLATE_URL': os.getenv('LIBRETRANSLATE_URL'),
'DB_TYPE': os.getenv('DB_TYPE'),
'DB_HOST': os.getenv('DB_HOST'),
'DB_PORT': os.getenv('DB_PORT'),
'DB_NAME': os.getenv('DB_NAME'),
'DB_USER': os.getenv('DB_USER')
}
# Obtener configuración cargada
config = get_config()
return templates.TemplateResponse("diagnosis.html", {
"request": request,
"env_vars": env_vars,
"config": config
})
if __name__ == "__main__":
import uvicorn
web_config = get_web_config()

View File

@@ -1,4 +1,5 @@
{% set lang = request.cookies.get('panel_lang', 'es') %}
{% set is_admin = username == 'nickpons666' %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
@@ -48,17 +49,17 @@
<div class="mb-3">
<label class="form-label">{{ "Token de Discord" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.discord.token }}" readonly>
value="{{ config.discord.token if is_admin else '****************************************' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ "Token de Telegram" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.telegram.token }}" readonly>
value="{{ config.telegram.token if is_admin else '****************************************' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ "URL de LibreTranslate" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.libretranslate.url }}" readonly>
value="{{ config.libretranslate.url if is_admin else '****************************************' }}" readonly>
</div>
</div>
</div>
@@ -107,28 +108,28 @@
<div class="mb-3">
<label class="form-label">{{ "Host MySQL" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.database.host }}" readonly>
value="{{ config.database.host if is_admin else '****************************************' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ "Puerto MySQL" | translate(lang) }}</label>
<input type="number" class="form-control"
value="{{ config.database.port }}" readonly>
value="{{ config.database.port if is_admin else '****************************************' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ "Usuario MySQL" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.database.user }}" readonly>
value="{{ config.database.user if is_admin else '****************************************' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">{{ "Nombre de Base de Datos" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.database.name }}" readonly>
value="{{ config.database.name if is_admin else '****************************************' }}" readonly>
</div>
{% else %}
<div class="mb-3">
<label class="form-label">{{ "Ruta de la base de datos" | translate(lang) }}</label>
<input type="text" class="form-control"
value="{{ config.database.path }}" readonly>
value="{{ config.database.path if is_admin else '****************************************' }}" readonly>
</div>
{% endif %}
</div>

View File

@@ -77,6 +77,16 @@
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-warning mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-hash"></i> {{ "Canales de Discord" | translate(lang) }}</h5>
<p class="card-text">{{ "Administrar canales habilitados para traducción" | translate(lang) }}</p>
<a href="/discord-channels" class="btn btn-dark btn-sm">{{ "Administrar" | translate(lang) }}</a>
</div>
</div>
</div>
</div>
<div class="card mt-4">

View File

@@ -0,0 +1,270 @@
{% set lang = request.cookies.get('panel_lang', 'es') %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ "Canales de Discord - Bots de Traducción" | translate(lang) }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
.channel-item {
border-left: 4px solid #6f42c1;
transition: all 0.3s ease;
}
.channel-item:hover {
border-left-color: #563d7c;
background-color: #f8f9fa;
}
.channel-inactive {
border-left-color: #dc3545;
opacity: 0.7;
}
.server-card {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 20px;
}
.server-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 8px 8px 0 0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #28a745;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<i class="bi bi-translate"></i> {{ "Bots de Traducción" | translate(lang) }}
</a>
<div class="d-flex align-items-center">
<div class="dropdown me-3">
<button class="btn btn-outline-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-translate"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item {{ 'active' if lang == 'es' }}" href="/set-lang/es">Español</a></li>
<li><a class="dropdown-item {{ 'active' if lang == 'en' }}" href="/set-lang/en">English</a></li>
<li><a class="dropdown-item {{ 'active' if lang == 'pt' }}" href="/set-lang/pt">Português</a></li>
</ul>
</div>
<a href="/dashboard" class="btn btn-outline-light btn-sm me-2">{{ "Dashboard" | translate(lang) }}</a>
<a href="/logout" class="btn btn-outline-light btn-sm">{{ "Cerrar Sesión" | translate(lang) }}</a>
</div>
</div>
</nav>
<div class="container mt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>📱 {{ "Canales de Discord" | translate(lang) }}</h2>
<div>
<a href="/diagnosis" class="btn btn-outline-secondary me-2">
<i class="bi bi-bug"></i> {{ "Diagnóstico" | translate(lang) }}
</a>
<form method="post" action="/discord-channels/sync" class="d-inline">
<button type="submit" class="btn btn-primary" onclick="return confirm('{{ '¿Sincronizar servidores y canales desde Discord?' | translate(lang) }}')">
<i class="bi bi-arrow-repeat"></i> {{ "Sincronizar" | translate(lang) }}
</button>
</form>
</div>
</div>
{% if success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> {{ "Configuración actualizada correctamente." | translate(lang) }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if synced %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> {{ "Servidores y canales sincronizados desde Discord." | translate(lang) }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-circle"></i> {{ "Error al realizar la operación. Verifica la configuración del bot." | translate(lang) }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if servers_with_channels|length == 0 %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> {{ "No hay servidores sincronizados. Usa el botón 'Sincronizar' para obtener los servidores y canales de Discord." | translate(lang) }}
</div>
{% else %}
{% for server_id, server_data in servers_with_channels.items() %}
<div class="card mb-4">
<div class="card-header bg-dark text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-server"></i> {{ server_data.server_info.server_name }}
<small class="text-muted">({{ server_data.server_info.server_id }})</small>
</h5>
<form method="post" action="/discord-channels/delete-server" class="d-inline">
<input type="hidden" name="server_id" value="{{ server_id }}">
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('{{ '¿Eliminar este servidor y todos sus canales?' | translate(lang) }}')">
<i class="bi bi-trash"></i> {{ "Eliminar" | translate(lang) }}
</button>
</form>
</div>
</div>
<div class="card-body">
{% if server_data.channels|length == 0 %}
<p class="text-muted">{{ "No hay canales en este servidor." | translate(lang) }}</p>
{% else %}
<form method="post" action="/discord-channels/bulk-toggle">
<input type="hidden" name="server_id" value="{{ server_id }}">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th width="50px">
<input type="checkbox" class="form-check-input" id="selectAll_{{ server_id }}"
onchange="toggleAllChannels({{ server_id }})">
</th>
<th>{{ "Canal" | translate(lang) }}</th>
<th>{{ "ID" | translate(lang) }}</th>
<th width="100px">{{ "Estado" | translate(lang) }}</th>
<th width="120px">{{ "Acciones" | translate(lang) }}</th>
</tr>
</thead>
<tbody>
{% for channel in server_data.channels %}
<tr>
<td>
<input type="checkbox" class="form-check-input channel-checkbox"
name="channel_ids" value="{{ channel.channel_id }}"
data-server="{{ server_id }}">
</td>
<td>
<i class="bi bi-hash"></i> {{ channel.channel_name }}
</td>
<td><code>{{ channel.channel_id }}</code></td>
<td>
{% if channel.is_active %}
<span class="badge bg-success">{{ "Activo" | translate(lang) }}</span>
{% else %}
<span class="badge bg-secondary">{{ "Inactivo" | translate(lang) }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="/discord-channels/toggle" class="d-inline">
<input type="hidden" name="channel_id" value="{{ channel.channel_id }}">
<input type="hidden" name="is_active" value="{{ 'false' if channel.is_active else 'true' }}">
<button type="submit" class="btn {% if channel.is_active %}btn-warning{% else %}btn-success{% endif %} btn-sm"
title="{{ 'Desactivar' if channel.is_active else 'Activar' | translate(lang) }}">
<i class="bi bi-{% if channel.is_active %}pause{% else %}play{% endif %}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<button type="submit" name="bulk_action" value="activate"
class="btn btn-success btn-sm me-2" disabled>
<i class="bi bi-play"></i> {{ "Activar seleccionados" | translate(lang) }}
</button>
<button type="submit" name="bulk_action" value="deactivate"
class="btn btn-warning btn-sm" disabled>
<i class="bi bi-pause"></i> {{ "Desactivar seleccionados" | translate(lang) }}
</button>
</div>
</form>
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function toggleAllChannels(serverId) {
const selectAll = document.getElementById('selectAll_' + serverId);
const checkboxes = document.querySelectorAll('.channel-checkbox[data-server="' + serverId + '"]');
const bulkButtons = selectAll.closest('form').querySelectorAll('button[type="submit"][name="bulk_action"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
// Habilitar/deshabilitar botones bulk
const anyChecked = document.querySelectorAll('.channel-checkbox:checked').length > 0;
bulkButtons.forEach(btn => btn.disabled = !anyChecked);
}
function updateBulkButtons() {
const anyChecked = document.querySelectorAll('.channel-checkbox:checked').length > 0;
document.querySelectorAll('button[type="submit"][name="bulk_action"]').forEach(btn => {
btn.disabled = !anyChecked;
});
// Actualizar checkboxes de "seleccionar todo"
document.querySelectorAll('[id^="selectAll_"]').forEach(selectAll => {
const serverId = selectAll.id.replace('selectAll_', '');
const checkboxes = document.querySelectorAll('.channel-checkbox[data-server="' + serverId + '"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
selectAll.checked = allChecked;
selectAll.indeterminate = anyChecked && !allChecked;
});
}
// Agregar event listeners a todos los checkboxes
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.channel-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBulkButtons);
});
});
</script>
</body>
</html>