diff --git a/app.py b/app.py index 9d15d20..bb50e03 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,8 @@ DB_CONFIG = { 'database': 'noticiasrss' } +MAX_FALLOS = 5 # Número máximo de fallos antes de desactivar el feed + # ====================================== # Página principal: últimas noticias # ====================================== @@ -90,9 +92,9 @@ def feeds(): try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() - # Feeds con descripción + # Feeds con descripción y fallos cursor.execute(""" - SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, f.pais_id, f.activo, c.nombre, p.nombre + SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, f.pais_id, f.activo, f.fallos, c.nombre, p.nombre FROM feeds f LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id @@ -193,7 +195,7 @@ def backup_feeds(): conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute(""" - SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, c.nombre AS categoria, f.pais_id, p.nombre AS pais, f.activo + SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, c.nombre AS categoria, f.pais_id, p.nombre AS pais, f.activo, f.fallos FROM feeds f LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id @@ -236,25 +238,26 @@ def restore_feeds(): n_ok = 0 for row in rows: try: - # Soporta CSV con o sin columna 'descripcion' descripcion = row.get('descripcion') or "" cursor.execute(""" - INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo) - VALUES (%s, %s, %s, %s, %s, %s) + INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo, fallos) + VALUES (%s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), descripcion=VALUES(descripcion), url=VALUES(url), categoria_id=VALUES(categoria_id), pais_id=VALUES(pais_id), - activo=VALUES(activo) + activo=VALUES(activo), + fallos=VALUES(fallos) """, ( row['nombre'], descripcion, row['url'], row['categoria_id'], row['pais_id'], - int(row['activo']) + int(row['activo']), + int(row.get('fallos', 0)) )) n_ok += 1 except Exception as e: @@ -268,29 +271,47 @@ def restore_feeds(): def show_noticias(): return home() +# ================================ +# Lógica de procesado de feeds con control de fallos +# ================================ +def sumar_fallo_feed(cursor, feed_id): + cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id = %s", (feed_id,)) + cursor.execute("SELECT fallos FROM feeds WHERE id = %s", (feed_id,)) + fallos = cursor.fetchone()[0] + if fallos >= MAX_FALLOS: + cursor.execute("UPDATE feeds SET activo = 0 WHERE id = %s", (feed_id,)) + return fallos + +def resetear_fallos_feed(cursor, feed_id): + cursor.execute("UPDATE feeds SET fallos = 0 WHERE id = %s", (feed_id,)) + def fetch_and_store(): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() - cursor.execute("SELECT url, categoria_id, pais_id FROM feeds WHERE activo = TRUE") + cursor.execute("SELECT id, url, categoria_id, pais_id FROM feeds WHERE activo = TRUE") feeds = cursor.fetchall() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] No se pudo conectar o leer feeds: {db_err}", exc_info=True) return - for rss_url, categoria_id, pais_id in feeds: + for feed_id, rss_url, categoria_id, pais_id in feeds: try: app.logger.info(f"Procesando feed: {rss_url} [{categoria_id}] [{pais_id}]") parsed = feedparser.parse(rss_url) except Exception as e: app.logger.error(f"[PARSE ERROR] Al parsear {rss_url}: {e}", exc_info=True) + sumar_fallo_feed(cursor, feed_id) continue if getattr(parsed, 'bozo', False): bozo_exc = getattr(parsed, 'bozo_exception', 'Unknown') app.logger.warning(f"[BOZO] Feed mal formado: {rss_url} - {bozo_exc}") + sumar_fallo_feed(cursor, feed_id) continue + else: + resetear_fallos_feed(cursor, feed_id) for entry in parsed.entries: link = entry.get('link') or entry.get('id') diff --git a/templates/base.html b/templates/base.html index b5b2a4d..f82e57f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,6 +31,7 @@ box-shadow: 0 1px 4px #0001; padding: 24px 20px 18px 20px; margin-bottom: 30px; + overflow-x: auto; } .btn { display: inline-block; @@ -152,6 +153,40 @@ .noticia-item { flex-direction: column; align-items: flex-start; gap: 9px; } .noticia-imagen { align-self: flex-start; } } + /* ------ BADGES DE ESTADO ------ */ + .badge-ok { + background: #ddffdd; + color: #1c8c1c; + font-weight: bold; + padding: 3px 14px; + border-radius: 8px; + font-size: 1em; + display: inline-block; + min-width: 38px; + text-align: center; + } + .badge-ko { + background: #ffdddd; + color: #b20000; + font-weight: bold; + padding: 3px 14px; + border-radius: 8px; + font-size: 1em; + display: inline-block; + min-width: 38px; + text-align: center; + } + .badge-warn { + background: #fff7cc; + color: #b68900; + font-weight: bold; + padding: 3px 14px; + border-radius: 8px; + font-size: 1em; + display: inline-block; + min-width: 38px; + text-align: center; + } /* Scroll suave para tablas grandes */ .card { overflow-x: auto; } diff --git a/templates/index.html b/templates/index.html index be77e4b..6d70212 100644 --- a/templates/index.html +++ b/templates/index.html @@ -56,12 +56,13 @@ URL Categoría País - Activo + Estado + Fallos Acciones - {% for id, nombre, descripcion, url, categoria_id, pais_id, activo, cat_nom, pais_nom in feeds %} + {% for id, nombre, descripcion, url, categoria_id, pais_id, activo, fallos, cat_nom, pais_nom in feeds %} {{ nombre }} @@ -72,15 +73,33 @@ {{ url }} {{ cat_nom or 'N/A' }} {{ pais_nom or 'N/A' }} - {{ 'Sí' if activo else 'No' }} + + {% if not activo %} + KO + {% elif fallos > 0 %} + ⚠️ + {% else %} + OK + {% endif %} + + + {% if fallos > 0 %} + {{ fallos }} + {% else %} + 0 + {% endif %} + Editar | Eliminar + {% if not activo %} + | Reactivar + {% endif %} {% else %} - No hay feeds aún. + No hay feeds aún. {% endfor %} @@ -101,7 +120,7 @@ optionNA.textContent = '— N/A —'; selectPais.appendChild(optionNA); paises.forEach(([id, nombre, contId]) => { - if (!continenteId || contId == continenteId) { + if (!continenteId || contId == continenteId || contId == Number(continenteId)) { const opt = document.createElement('option'); opt.value = id; opt.textContent = nombre;