Resuelto conflicto en X

This commit is contained in:
jlimolina 2025-06-09 11:48:00 +02:00
parent 758da0ad4c
commit e9264bc6ce
9 changed files with 348 additions and 96 deletions

94
app.py
View file

@ -4,6 +4,7 @@ import sys
import hashlib
import re
import csv
import math
from io import StringIO
from datetime import datetime, timedelta
import logging
@ -69,33 +70,45 @@ def home():
cat_id=int(cat_id) if cat_id else None, cont_id=int(cont_id) if cont_id else None, pais_id=int(pais_id) if pais_id else None)
@app.route("/feeds")
def feeds():
feeds_, categorias, continentes, paises = [], [], [], []
def dashboard():
stats = {'feeds_totales': 0, 'noticias_totales': 0, 'feeds_caidos': 0}
try:
with get_conn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT COUNT(*) FROM feeds;")
stats['feeds_totales'] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM noticias;")
stats['noticias_totales'] = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM feeds WHERE activo = FALSE;")
stats['feeds_caidos'] = cursor.fetchone()[0]
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al calcular estadísticas del dashboard: {db_err}")
flash("Error al conectar con la base de datos para mostrar el resumen.", "error")
return render_template("dashboard.html", stats=stats)
@app.route("/feeds/manage")
def manage_feeds():
page = request.args.get('page', 1, type=int)
per_page = 10
offset = (page - 1) * per_page
feeds_list = []
total_feeds = 0
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("""
SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, f.pais_id,
f.activo, f.fallos, c.nombre as cat_nom, p.nombre as pais_nom
FROM feeds f
LEFT JOIN categorias c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id
ORDER BY f.nombre
""")
feeds_ = cursor.fetchall()
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
categorias = cursor.fetchall()
cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre")
continentes = cursor.fetchall()
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
paises = cursor.fetchall()
cursor.execute("SELECT COUNT(*) FROM feeds")
total_feeds = cursor.fetchone()[0]
cursor.execute("SELECT * FROM feeds ORDER BY nombre LIMIT %s OFFSET %s", (per_page, offset))
feeds_list = cursor.fetchall()
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al leer feeds: {db_err}", exc_info=True)
flash("Error de base de datos al cargar la gestión de feeds.", "error")
return render_template("index.html", feeds=feeds_, categorias=categorias, continentes=continentes, paises=paises)
app.logger.error(f"[DB ERROR] Al obtener lista de feeds: {db_err}")
flash("Error al obtener la lista de feeds.", "error")
total_pages = math.ceil(total_feeds / per_page)
return render_template("feeds_list.html", feeds=feeds_list, page=page, total_pages=total_pages, total_feeds=total_feeds)
@app.route("/add", methods=["POST"])
@app.route("/feeds/add", methods=['GET', 'POST'])
def add_feed():
if request.method == 'POST':
nombre = request.form.get("nombre")
try:
with get_conn() as conn:
@ -108,7 +121,20 @@ def add_feed():
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al agregar feed: {db_err}", exc_info=True)
flash(f"Error al añadir el feed: {db_err}", "error")
return redirect(url_for("feeds"))
return redirect(url_for("dashboard"))
categorias, paises = [], []
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
categorias = cursor.fetchall()
cursor.execute("SELECT id, nombre FROM paises ORDER BY nombre")
paises = cursor.fetchall()
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al cargar formulario para añadir feed: {db_err}")
flash("No se pudieron cargar las categorías o países.", "error")
return render_template("add_feed.html", categorias=categorias, paises=paises)
@app.route("/edit/<int:feed_id>", methods=["GET", "POST"])
def edit_feed(feed_id):
@ -124,8 +150,7 @@ def edit_feed(feed_id):
(request.form.get("nombre"), request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), idioma, activo, feed_id)
)
flash("Feed actualizado correctamente.", "success")
return redirect(url_for("feeds"))
return redirect(url_for("manage_feeds"))
cursor.execute("SELECT * FROM feeds WHERE id = %s", (feed_id,))
feed = cursor.fetchone()
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
@ -135,12 +160,10 @@ def edit_feed(feed_id):
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al editar feed: {db_err}", exc_info=True)
flash(f"Error al editar el feed: {db_err}", "error")
return redirect(url_for("feeds"))
return redirect(url_for("manage_feeds"))
if not feed:
flash("No se encontró el feed solicitado.", "error")
return redirect(url_for("feeds"))
return redirect(url_for("manage_feeds"))
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
@app.route("/delete/<int:feed_id>")
@ -153,7 +176,7 @@ def delete_feed(feed_id):
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al eliminar feed: {db_err}", exc_info=True)
flash(f"Error al eliminar el feed: {db_err}", "error")
return redirect(url_for("feeds"))
return redirect(url_for("manage_feeds"))
@app.route("/reactivar_feed/<int:feed_id>")
def reactivar_feed(feed_id):
@ -165,7 +188,7 @@ def reactivar_feed(feed_id):
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al reactivar feed: {db_err}", exc_info=True)
flash(f"Error al reactivar el feed: {db_err}", "error")
return redirect(url_for("feeds"))
return redirect(url_for("manage_feeds"))
@app.route("/backup_feeds")
def backup_feeds():
@ -183,21 +206,18 @@ def backup_feeds():
feeds_ = cursor.fetchall()
if not feeds_:
flash("No hay feeds para exportar.", "warning")
return redirect(url_for("feeds"))
return redirect(url_for("dashboard"))
si = StringIO()
writer = csv.DictWriter(si, fieldnames=[desc[0] for desc in cursor.description])
writer.writeheader()
writer.writerows([dict(row) for row in feeds_])
output = si.getvalue()
si.close()
return Response(output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"})
except Exception as e:
app.logger.error(f"[ERROR] Al hacer backup de feeds: {e}", exc_info=True)
flash("Error al generar el backup.", "error")
return redirect(url_for("feeds"))
return redirect(url_for("dashboard"))
@app.route("/restore_feeds", methods=["GET", "POST"])
def restore_feeds():
@ -211,7 +231,6 @@ def restore_feeds():
reader = csv.DictReader(file_stream)
rows = list(reader)
n_ok, n_err = 0, 0
with get_conn() as conn:
with conn.cursor() as cursor:
for row in rows:
@ -242,8 +261,7 @@ def restore_feeds():
except Exception as e:
app.logger.error(f"Error al restaurar feeds desde CSV: {e}", exc_info=True)
flash(f"Ocurrió un error general al procesar el archivo: {e}", "error")
return redirect(url_for("feeds"))
return redirect(url_for("dashboard"))
return render_template("restore_feeds.html")
def sumar_fallo_feed(cursor, feed_id):

57
templates/add_feed.html Normal file
View file

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Añadir Nuevo Feed{% endblock %}
{% block content %}
<header>
<h1>Añadir Nuevo Feed</h1>
<p class="subtitle">Introduce los detalles de la nueva fuente de noticias RSS.</p>
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
</header>
<div class="form-section">
<form action="{{ url_for('add_feed') }}" method="post" autocomplete="off">
<div>
<label for="nombre">Nombre del feed</label>
<input id="nombre" name="nombre" type="text" placeholder="Ej: Noticias de Tecnología" required>
</div>
<div style="margin-top:15px;">
<label for="url">URL del RSS</label>
<input id="url" name="url" type="url" placeholder="https://ejemplo.com/rss" required>
</div>
<div style="margin-top:15px;">
<label for="descripcion">Descripción</label>
<textarea id="descripcion" name="descripcion" rows="2" placeholder="Breve descripción del contenido del feed"></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
<div>
<label for="categoria_id">Categoría</label>
<select id="categoria_id" name="categoria_id" required>
<option value="">— Elige categoría —</option>
{% for cat in categorias %}
<option value="{{ cat.id }}">{{ cat.nombre }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="pais_id">País</label>
<select name="pais_id" id="pais_id">
<option value="">— Global / No aplica —</option>
{% for pais in paises %}
<option value="{{ pais.id }}">{{ pais.nombre }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="idioma">Idioma (código)</label>
<input id="idioma" name="idioma" type="text" maxlength="2" placeholder="ej: es, en">
</div>
</div>
<button type="submit" class="btn" style="margin-top: 25px; width: 100%;">Añadir Feed</button>
</form>
</div>
{% endblock %}

57
templates/add_feeds.html Normal file
View file

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Añadir Nuevo Feed{% endblock %}
{% block content %}
<header>
<h1>Añadir Nuevo Feed</h1>
<p class="subtitle">Introduce los detalles de la nueva fuente de noticias RSS.</p>
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
</header>
<div class="form-section">
<form action="{{ url_for('add_feed') }}" method="post" autocomplete="off">
<div>
<label for="nombre">Nombre del feed</label>
<input id="nombre" name="nombre" type="text" placeholder="Ej: Noticias de Tecnología" required>
</div>
<div style="margin-top:15px;">
<label for="url">URL del RSS</label>
<input id="url" name="url" type="url" placeholder="https://ejemplo.com/rss" required>
</div>
<div style="margin-top:15px;">
<label for="descripcion">Descripción</label>
<textarea id="descripcion" name="descripcion" rows="2" placeholder="Breve descripción del contenido del feed"></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px;">
<div>
<label for="categoria_id">Categoría</label>
<select id="categoria_id" name="categoria_id" required>
<option value="">— Elige categoría —</option>
{% for cat in categorias %}
<option value="{{ cat.id }}">{{ cat.nombre }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="pais_id">País</label>
<select name="pais_id" id="pais_id">
<option value="">— Global / No aplica —</option>
{% for pais in paises %}
<option value="{{ pais.id }}">{{ pais.nombre }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="idioma">Idioma (código)</label>
<input id="idioma" name="idioma" type="text" maxlength="2" placeholder="ej: es, en">
</div>
</div>
<button type="submit" class="btn" style="margin-top: 25px; width: 100%;">Añadir Feed</button>
</form>
</div>
{% endblock %}

View file

@ -67,6 +67,7 @@
.btn, button { padding: 12px 25px; background: var(--gradiente-principal); color: white !important; border: none; border-radius: var(--border-radius-sm); font-size: 1rem; font-weight: 600; cursor: pointer; transition: all var(--transition-speed) ease; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); text-decoration: none; display: inline-block; text-align: center; }
.btn:hover, button:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); text-decoration: none; }
.btn-secondary { background: #34495e; } .btn-secondary:hover { background: #2c3e50; }
.btn-small { padding: 6px 14px; font-size: 0.9rem; }
a { color: var(--secondary-color); text-decoration: none; font-weight: 500; } a:hover { text-decoration: underline; }
.top-link { display: inline-block; margin-bottom: 25px; font-weight: 500; color: var(--primary-color); }
.top-link:hover { text-decoration: underline; }
@ -82,14 +83,6 @@
.noticia-texto h3 a:hover { color: var(--primary-color); }
.noticia-meta { font-size: 0.8rem; color: var(--text-color-light); margin-bottom: 8px; }
/* --- Tabla de Gestión de Feeds --- */
.table-wrapper { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border-color); }
th { background-color: rgba(106, 27, 203, 0.05); font-weight: 600; }
tr:last-child td { border-bottom: none; }
td .actions a { margin-right: 10px; }
/* --- Alertas y Mensajes Flash --- */
.flash-messages { list-style: none; padding: 0; margin-bottom: 20px; }
.flash-messages li { padding: 15px 20px; border-radius: var(--border-radius-sm); border-left: 5px solid; }
@ -97,11 +90,33 @@
.flash-messages .success { background-color: #e6fcf5; color: #00b894; border-color: #00b894; }
.flash-messages .warning { background-color: #fffbeb; color: #f39c12; border-color: #f39c12; }
/* --- INICIO DE ESTILOS AÑADIDOS PARA DASHBOARD Y PAGINACIÓN --- */
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }
.stat-card { background: rgba(255, 255, 255, 0.8); padding: 20px; border-radius: var(--border-radius-md); text-align: center; border: 1px solid var(--border-color); transition: all 0.3s ease; }
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 4px 15px rgba(0,0,0,0.08); }
.stat-card .stat-number { font-size: 2.5rem; font-weight: 600; background: var(--gradiente-principal); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.2; }
.stat-card .stat-label { font-size: 0.9rem; color: var(--text-color-light); font-weight: 500; margin-top: 5px; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 5px; margin: 30px 0; flex-wrap: wrap; }
.page-link { display: inline-block; padding: 8px 14px; background: rgba(255, 255, 255, 0.6); border: 1px solid var(--border-color); border-radius: var(--border-radius-sm); color: var(--primary-color); text-decoration: none; transition: all 0.2s ease; }
.page-link:hover { background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
.page-link.active { background: var(--gradiente-principal); color: white; border-color: transparent; cursor: default; }
.feed-detail-card { padding: 0; }
.feed-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; background: rgba(233, 236, 239, 0.5); padding: 15px 25px; border-bottom: 1px solid var(--border-color); }
.feed-header h2 { margin: 0; font-size: 1.4rem; }
.feed-body { padding: 25px; }
.feed-body dl { display: grid; grid-template-columns: 120px 1fr; gap: 10px 20px; }
.feed-body dt { font-weight: 600; color: var(--text-color-light); }
.feed-body dd { margin: 0; word-break: break-all; }
/* --- FIN DE ESTILOS AÑADIDOS --- */
/* --- Responsividad --- */
@media (max-width: 768px) {
.container { padding: 20px; margin: 15px; }
h1 { font-size: 2rem; }
.noticia-item { flex-direction: column; }
.feed-body dl { grid-template-columns: 100px 1fr; }
}
</style>
</head>
@ -111,7 +126,7 @@
{% if messages %}
<ul class="flash-messages">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
<li class="flash-{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}

35
templates/dashboard.html Normal file
View file

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Dashboard de Feeds{% endblock %}
{% block content %}
<header>
<h1>Dashboard de Feeds</h1>
<p class="subtitle">Un resumen del estado de tu agregador de noticias.</p>
<a href="{{ url_for('home') }}" class="top-link" style="margin-top:15px;">← Volver a las Noticias</a>
</header>
<div class="dashboard-grid">
<div class="stat-card">
<div class="stat-number">{{ stats.feeds_totales }}</div>
<div class="stat-label">Feeds Totales</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.noticias_totales }}</div>
<div class="stat-label">Noticias Recopiladas</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color:#c0392b;">{{ stats.feeds_caidos }}</div>
<div class="stat-label">Feeds Caídos / Inactivos</div>
</div>
</div>
<div class="card" style="text-align: center; padding: 30px;">
<h2>Gestionar Feeds</h2>
<p style="color: var(--text-color-light);">Aquí puedes ver la lista completa, editar, añadir o eliminar tus feeds.</p>
<div style="margin-top: 20px; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap;">
<a href="{{ url_for('manage_feeds') }}" class="btn">Ver Lista Detallada</a>
<a href="{{ url_for('add_feed') }}" class="btn">Añadir Nuevo Feed</a>
<a href="{{ url_for('restore_feeds') }}" class="btn btn-secondary">Importar / Restaurar</a>
</div>
</div>
{% endblock %}

View file

@ -9,7 +9,7 @@
</header>
<div class="form-section">
<form method="post" autocomplete="off">
<form method="post" action="{{ url_for('edit_feed', feed_id=feed.id) }}" autocomplete="off">
<div>
<label for="nombre">Nombre del feed</label>
<input id="nombre" name="nombre" type="text" value="{{ feed.nombre }}" required>
@ -57,8 +57,10 @@
</div>
<button class="btn" type="submit">Guardar Cambios</button>
<a href="{{ url_for('feeds') }}" style="margin-left: 15px;">Cancelar</a>
<a href="{{ url_for('manage_feeds') }}" style="margin-left: 15px;">Cancelar</a>
</form>
</div>
<a href="{{ url_for('feeds') }}" class="top-link">← Volver a la gestión de feeds</a>
<a href="{{ url_for('manage_feeds') }}" class="top-link">← Volver a la lista de feeds</a>
{% endblock %}

64
templates/feeds_list.html Normal file
View file

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}Lista Detallada de Feeds{% endblock %}
{% block content %}
<header>
<h1>Lista de Feeds</h1>
<p class="subtitle">Mostrando {{ feeds|length }} de {{ total_feeds }} feeds. Página {{ page }} de {{ total_pages }}.</p>
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
</header>
{% if total_pages > 1 %}
<nav class="pagination">
{% if page > 1 %}
<a href="{{ url_for('manage_feeds', page=page-1) }}" class="page-link">&laquo; Anterior</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<a href="#" class="page-link active">{{ p }}</a>
{% else %}
<a href="{{ url_for('manage_feeds', page=p) }}" class="page-link">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="{{ url_for('manage_feeds', page=page+1) }}" class="page-link">Siguiente &raquo;</a>
{% endif %}
</nav>
{% endif %}
{% for feed in feeds %}
<div class="card feed-detail-card">
<div class="feed-header">
<h2>{{ feed.nombre }}</h2>
<div class="actions">
<a href="{{ url_for('edit_feed', feed_id=feed.id) }}" class="btn btn-small">Editar</a>
</div>
</div>
<div class="feed-body">
<dl>
<dt>ID:</dt><dd>{{ feed.id }}</dd>
<dt>URL:</dt><dd><a href="{{ feed.url }}" target="_blank" rel="noopener">{{ feed.url }}</a></dd>
<dt>Descripción:</dt><dd>{{ feed.descripcion or 'N/A' }}</dd>
<dt>Idioma:</dt><dd>{{ feed.idioma or 'N/D' }}</dd>
<dt>Estado:</dt><dd>
{% if feed.activo %}
<span style="color: #27ae60; font-weight:bold;">Activo</span>
{% else %}
<span style="color: #c0392b; font-weight: bold;">Inactivo</span>
{% endif %}
</dd>
<dt>Fallos:</dt><dd>{{ feed.fallos }}</dd>
</dl>
</div>
</div>
{% endfor %}
{% if not feeds %}
<div class="card" style="text-align:center;">
<p>No hay feeds para mostrar.</p>
</div>
{% endif %}
{% endblock %}

View file

@ -6,7 +6,7 @@
<header>
<h1>Agregador de Noticias</h1>
<p class="subtitle">Tus fuentes de información, en un solo lugar.</p>
<a href="{{ url_for('feeds') }}" class="top-link" style="margin-top:15px;">⚙️ Gestionar Feeds</a>
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">⚙️ Gestionar Feeds</a>
</header>
<div class="card">

View file

@ -1,33 +1,37 @@
{% extends "base.html" %}
{% block title %}Restaurar Feeds RSS{% endblock %}
{% block content %}
<h1>Restaurar feeds desde backup CSV</h1>
<div class="card" style="max-width:500px;margin:auto;">
<form method="post" enctype="multipart/form-data" autocomplete="off">
<label for="file"><strong>Selecciona el archivo CSV exportado de feeds:</strong></label>
<input type="file" name="file" id="file" accept=".csv" required style="margin:10px 0;">
<button class="btn" type="submit">Restaurar</button>
</form>
{% if msg %}
<div style="margin:15px 0;">
{% if "Error" in msg or "Error en fila" in msg %}
<div style="color:#c00; font-weight:bold;">{{ msg|safe }}</div>
{% else %}
<div style="color:#198754; font-weight:bold;">{{ msg|safe }}</div>
{% endif %}
</div>
{% endif %}
<p style="font-size:0.92em;color:#64748b;">
El archivo debe contener las columnas:<br>
<code>id, nombre, [descripcion,] url, categoria_id, categoria, pais_id, pais, idioma, activo, fallos</code><br>
<small>
Las columnas <b>descripcion</b> e <b>idioma</b> son opcionales.<br>
<b>activo</b> puede ser: <code>True</code>, <code>False</code>, <code>1</code> o <code>0</code>.<br>
<b>idioma</b> debe ser el código ISO 639-1 de dos letras (<i>ej:</i> <code>es</code>, <code>en</code>, <code>fr</code>...).<br>
Si falta alguna columna, la restauración puede fallar o ignorar ese campo.
</small>
</p>
</div>
<a href="/feeds" class="top-link">← Volver a feeds</a>
{% endblock %}
{% block title %}Restaurar Feeds desde Backup{% endblock %}
{% block content %}
<header>
<h1>Restaurar Feeds</h1>
<p class="subtitle">Importa todos tus feeds desde un único archivo de backup en formato CSV.</p>
<a href="{{ url_for('dashboard') }}" class="top-link" style="margin-top:15px;">← Volver al Dashboard</a>
</header>
<div class="form-section">
<h2>Subir Archivo de Backup</h2>
<form method="post" enctype="multipart/form-data" action="{{ url_for('restore_feeds') }}" autocomplete="off">
<label for="file"><strong>Selecciona el archivo <code>feeds_backup.csv</code>:</strong></label>
<input type="file" name="file" id="file" accept=".csv" required style="margin-top:10px;">
<button class="btn" type="submit" style="margin-top:20px; width: 100%;">Iniciar Restauración</button>
</form>
</div>
<div class="card">
<h2>Formato del Archivo CSV</h2>
<p style="color: var(--text-color-light);">
El archivo debe ser de tipo CSV y contener las siguientes columnas para que la importación funcione correctamente:
</p>
<code>id,nombre,descripcion,url,categoria_id,categoria,pais_id,pais,idioma,activo,fallos</code>
<br><br>
<small>
<ul>
<li>La columna `id` debe ser única para cada feed.</li>
<li>La columna `url` también debe ser única.</li>
<li><b>activo</b> puede ser: <code>True</code>, <code>False</code>, <code>1</code> o <code>0</code>.</li>
<li><b>idioma</b> debe ser el código ISO 639-1 de dos letras (ej: <code>es</code>, <code>en</code>).</li>
</ul>
</small>
</div>
{% endblock %}