rss2/plan-app.md
2026-08-28 21:10:02 +02:00

71 KiB
Raw Permalink Blame History

Plan de Migración de RSS2 a Aplicación Multiplataforma

Versión: 1.0 Fecha: 2026-08-14 Estado: Propuesta de arquitectura y plan de migración


Tabla de Contenidos

  1. Resumen Ejecutivo
  2. Análisis Profundo del Código Actual
  3. Objetivos y Requisitos de la Nueva Aplicación
  4. Arquitectura Objetivo (Alternativas)
  5. Selección Tecnológica por Plataforma
  6. Estrategia de Datos y Sincronización
  7. Estrategia de IA/ML en el Dispositivo
  8. Núcleo Embebido (Embedded Backend)
  9. Rediseño de la API para Clientes Nativos
  10. Migración del Frontend Paso a Paso
  11. Empaquetado y Distribución por Plataforma
  12. Infraestructura de Build y CI/CD
  13. Plan de Implementación por Fases
  14. Riesgos y Mitigaciones
  15. Estrategia de Testing y QA
  16. Referencias y Documentación

1. Resumen Ejecutivo

RSS2 es actualmente una aplicación web monolítica orquestada con Docker Compose compuesta por más de 20 servicios interconectados: un frontend React SPA, un backend Go (API REST Gin + 6 workers de ingesta/enriquecimiento), 11 workers Python (traducción neuronal, embeddings, NER, clustering, detección de idioma, categorización LLM) y tres sistemas de almacenamiento (PostgreSQL, Redis, Qdrant).

El objetivo de este plan es transformar este sistema en una aplicación nativa instalable en Windows, Linux, Android e iOS. Dada la enorme complejidad del stack actual (modelos ML de 600MB–1.3B, vector DB, colas, 20 servicios), la estrategia recomendada es una arquitectura cliente–servidor con capacidades offline-first:

  • El "cerebro" del sistema (ingesta, IA, almacenamiento) sigue corriendo como un servidor (Docker o binario nativo en un equipo potente).
  • Las apps nativas (desktop y móviles) son clientes ricos con caché local SQLite, modo offline, notificaciones push y, en escritorios con GPU, capacidad de actuar como workers remotos de traducción (reutilizando la infraestructura WebSocket ya existente).

La clave del éxito es el altísimo grado de reutilización del frontend React actual: la misma base de código puede empaquetarse con Tauri (Windows/Linux) y con Capacitor (Android/iOS), requiriendo únicamente abstracciones sobre las APIs del navegador.


2. Análisis Profundo del Código Actual

2.1 Arquitectura General

                        ┌────────────────────────────────────────────┐
                        │             CLIENTE WEB (Navegador)        │
                        │    React 18 + Vite + Tailwind + React Query │
                        └───────────────────┬────────────────────────┘
                                            │ HTTP/JSON /api + JWT
                                            ▼
                        ┌────────────────────────────────────────────┐
                        │              nginx (puerto 8888)           │
                        │   Gateway + proxy inverso + estáticos SPA  │
                        └───────┬─────────────────────────┬──────────┘
                                │ /api/                    │ / (estáticos)
                                ▼                          ▼
                        ┌────────────────────┐      ┌────────────────────┐
                        │    backend-go      │      │    rss2_frontend   │
                        │  Gin REST API :8080│      │  React compilado   │
                        └───────┬──────┬─────┘      └────────────────────┘
              ┌─────────────────┘      └───────────────┬────────────────────┐
              ▼                                        ▼                    ▼
        PostgreSQL 18                            Redis 7              Qdrant
        (esquema relacional)                  (caché/colas)        (vector DB)
              ▲
              │   Red interna de Docker Compose (backend network)
   ┌──────────┴─────────────────────────────────────────────────────────────┐
   │  Workers Go                    Workers Python                          │
   │  ────────────                  ──────────────                          │
   │  rss-ingestor-go               langdetect_worker.py                    │
   │  scraper (deep scraping)       ctranslator_worker.py (NLLB-200)        │
   │  discovery (descubrir feeds)   embeddings_worker.py (MiniLM)           │
   │  wiki_worker (Wikipedia)       ner_worker.py (spaCy es_core_news_lg)   │
   │  topics (países/temas)         cluster_worker.py                       │
   │  related (similitud coseno)    translation_scheduler.py                │
   │  qdrant (vectorización)        llm_categorizer_worker.py (Ollama)      │
   │                                 remote_translator_worker.py (WS GPU)   │
   └────────────────────────────────────────────────────────────────────────┘

Topología de redes (docker-compose.yml):

  • Red frontend: nginx, rss2_frontend, backend-go (puente entre redes).
  • Red backend: db, redis, ingestor, langdetect, scraper, discovery, wiki-worker, translator, translator-gpu, scheduler, embeddings, topics, related, qdrant, qdrant-worker, ner, cluster.
  • Único puerto expuesto al exterior: nginx :8888.

2.2 Inventario de Servicios (docker-compose.yml, 20 servicios)

Servicio Imagen/Dockerfile Rol Recursos
db postgres:18-alpine Base de datos relacional 2 CPU / 8G
redis redis:7-alpine Caché + colas (workers Python) 768M
rss-ingestor-go rss-ingestor-go/Dockerfile Crawler RSS (100 workers) 2 CPU / 2G
langdetect Dockerfile Detección de idioma 0.5 CPU / 512M
scraper Dockerfile.scraper Extracción profunda de artículos 1 CPU / 512M
discovery Dockerfile.discovery Descubrimiento de feeds 1 CPU / 512M
wiki-worker Dockerfile.wiki Enriquecimiento Wikipedia 0.5 CPU / 256M
backend-go backend/Dockerfile API REST principal 2 CPU / 512M
rss2_frontend frontend/Dockerfile SPA React 1 CPU / 512M
nginx nginx:alpine Gateway —
translator Dockerfile.translator Traducción NLLB CPU 4 CPU / 6G
translator-gpu Dockerfile.translator 2º worker CPU (nombre heredado) 4 CPU / 6G
translation-scheduler Dockerfile.scheduler Crea trabajos de traducción 0.5 CPU / 256M
embeddings Dockerfile Vectores MiniLM 2 CPU / 6G
topics Dockerfile.topics Matcher países/temas 1 CPU / 512M
related Dockerfile.related Noticias relacionadas 1 CPU / 1G
qdrant qdrant/qdrant:latest Base vectorial 4 CPU / 4G
qdrant-worker Dockerfile.qdrant Vectorización → Qdrant 1 CPU / 1G
ner Dockerfile NER spaCy 2 CPU / 2G
cluster Dockerfile Clustering de noticias 2 CPU / 2G

Observación crítica para la migración: el servicio translator-gpu NO usa CUDA; es un segundo worker CPU. La única capacidad GPU real es vía workers remotos WebSocket (remote_translator_worker.py). Esto es una ventaja enorme: la infraestructura para que un equipo externo (potencialmente una app de escritorio) traduzca vía WebSocket ya existe y está probada.

2.3 Frontend (React + TypeScript + Vite)

Ubicación: frontend/src — 26 archivos, ~4.300 líneas TS/TSX.

Stack: React 18.2, TypeScript 5.3 (strict), Vite 5, Tailwind CSS 3.4, React Router 6, TanStack React Query 5, Axios, lucide-react (iconos), date-fns.

Estructura:

  • main.tsx → QueryClientProvider → BrowserRouter → App.
  • App.tsx: llama a GET /auth/check-first-user; si es el primer usuario muestra WelcomeWizard (onboarding de 3 pasos), si no, el árbol de rutas envuelto en Layout.
  • 18 páginas: Home, News, Search, Feeds, Stats, Favorites, Account, Login, Populares (732 líneas, la más compleja), Analisis, Alertas, WelcomeWizard, AdminAliases, AdminUsers, AdminSettings, AdminWorkers.
  • 4 componentes reutilizables: Layout, ProtectedRoute, LineChart (SVG manual), WikiTooltip.
  • services/api.ts (279 líneas): instancia Axios, interceptor de petición (inyecta JWT de localStorage), interceptor de respuesta (401 → redirige a /login?expired=1), tipos TS e apiService (22 métodos tipados) + exportación del cliente crudo api.

Dependencias de navegador que bloquean una migración nativa directa:

API web actual Uso Sustitución nativa
localStorage (token, user, favorites) Almacenar sesión y favoritos SecureStorage/Keychain/Keyring (Electron/Tauri/Capacitor) + SQLite
window.location.href (redirect 401) Forzar logout Callback de sesión expirada → navegación interna
window.confirm/prompt/alert Confirmar borrados, doble confirmación ("type SI"), etc. Diálogos nativos (capas JS→Rust/ObjC/Swift/Kotlin) o modales React
URL.createObjectURL + <a download> Descarga de CSV/ZIP/SQL API de archivos nativa (saveDialog en Tauri/Electron, Share/filesystem en Capacitor)
window.addEventListener('storage') Sync de auth entre pestañas Eventos de sesión dentro de la misma app
window.history (BrowserRouter) Navegación HashRouter/MemoryRouter o navegación nativa
window.URLSearchParams Parámetros de ruta React Router compatible
navigator/Viewport Responsividad Adaptación a pantallas táctiles

Deudas técnicas detectadas en el frontend (deben corregirse durante la migración):

  1. Nav móvil rota: hidden sm:flex sin hamburguesa → inutilizable en móvil (< 640px).
  2. WikiTooltip solo responde a hover → sin soporte táctil.
  3. Página Favorites usa solo localStorage (no sincronizada con backend, pese a existir la tabla favoritos).
  4. Account no persiste cambios (guardado falso).
  5. Botón btn-danger usado pero no definido en index.css.
  6. Dos patrones de fetch inconsistentes: React Query vs useEffect+fetch manual (Populares, Alertas, Analisis, Admin*).
  7. Dos clientes de API: api crudo vs apiService tipado.
  8. Filtros de categoría/país en Search son "muertos" (no se envían al backend).
  9. clsx y tailwind-merge instalados pero sin uso.
  10. No existe ni un solo test pese a vitest configurado.
  11. Sin code-splitting (React.lazy ausente); todo el bundle en un solo archivo.
  12. JWT_EXPIRATION del backend ignorado; el token expira en 24h fijas sin refresh — crítico para apps móviles.

2.4 Backend Go (API + Workers)

Módulo: github.com/rss2/backend, Go 1.23, gin v1.9.1, pgx/v5, go-redis/v9, golang-jwt/v5, gorilla/websocket, gofeed, goquery.

Paquetes internos:

  • internal/config — Load() lee variables de entorno (sin archivos .env; todo lo inyecta Docker).
  • internal/db — pgxpool (25 conns max), solo para la API (DATABASE_URL).
  • internal/cache — Cliente Redis (helpers nunca usados por handlers; solo conexión).
  • internal/auth — JWT HS256, expiración 24h hardcodeada.
  • internal/middleware — AuthRequired, AdminRequired, CORS. RateLimit es un stub no conectado.
  • internal/models — Tipos de dominio. ⚠️ News.ID es int64 pero la DB usa VARCHAR(32) (MD5 de la URL).
  • internal/handlers — auth, news, feed, search, alerts, admin, remote_worker, worker_ws.
  • internal/services/ml.go — Clientes HTTP hacia Libretranslate/Ollama/spaCy; SemanticSearch es stub que devuelve vacío.
  • internal/workers/db.go — Helper de conexión compartido por los 6 workers (usa DB_* env vars).

Arranque del servidor (cmd/server/main.go):

  1. config.Load() → 2. db.Connect() (fatal si falla) → 3. initDB() (migraciones idempotentes: crea entity_aliases, config, alertas, remote_workers; añade role; seed de config) → 4. cache.Connect() (no fatal) → 5. services.Init() → 6. router Gin → 7. CORS → 8. rutas → 9. auth.SetJWTSecret() → 10. r.Run(":8080") → 11. goroutines StartJobAssigner() (cada 5s) y StartAlertScanner() (cada 120 min) → 12. señal SIGINT/SIGTERM para shutdown graceful.

Workers Go (binarios independientes, todos con bucle time.Ticker):

Worker Loop Función
scraper 60s Enriquece noticias con resumen corto (<200 chars) vía goquery; extrae artículos de fuentes_url (OpenGraph + selectores).
discovery 900s Detecta feeds en fuentes_url; crea feeds o feeds_pending (aprobación manual).
wiki_worker 30s Consulta la API REST de Wikipedia ES; descarga thumbnails (≤2MB) a data/wiki_images; rellena wiki_summary, wiki_url, image_path.
topics 10s Matchea keywords de topics contra noticias sin procesar; asigna pais_id.
related 10s Similitud coseno entre embeddings almacenados; rellena related_noticias.
qdrant 30s Toma traducciones sin vectorizar, parsea el embedding del array Postgres, hace upsert en colección news_vectors de Qdrant.

WebSocket /ws/worker (handlers/worker_ws.go):

  • Protocolo existente y probado para workers remotos GPU.
  • Auth por api_key (query o header X-API-Key) contra tabla remote_workers.
  • Registro, heartbeat (10s), ping (30s), asignación de jobs con FOR UPDATE SKIP LOCKED, límite de 5 min en assigned, re-encolado tras desconexión.
  • El endpoint se registra fuera de /api (en /ws/worker) y no está proxied por el nginx de producción — quirk a documentar.

2.5 Workers Python

Dependencias (requirements.txt): Flask, feedparser, APScheduler, psycopg2, transformers 4.43, ctranslate2, sentence-transformers 3.0.1, spaCy 3.7, sklearn, qdrant-client 1.11, redis, langdetect, newspaper3k, etc.

Worker Modelo Recursos Comportamiento
ctranslator_worker.py NLLB-200 distilled 600M (CT2, int8) 4 CPU / 6G Loop 30s; FOR UPDATE SKIP LOCKED; batch 128; cache Redis por título/cuerpo; trocea cuerpos en chunks de 900 chars; executemany; registra translation_stats.
embeddings_worker.py sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 2 CPU / 6G model.encode(normalize_embeddings=True); upsert en traduccion_embeddings; sleep 5s idle.
ner_worker.py spaCy es_core_news_lg 2 CPU / 2G Extrae PER/ORG/LOC → tags y tags_noticia.
cluster_worker.py embeddings MiniLM + sklearn 2 CPU / 2G Agrupa noticias en eventos (umbral 0.35).
langdetect_worker.py langdetect 0.5 CPU Detecta idioma → noticias.lang.
llm_categorizer_worker.py Ollama/Mistral-7B (externa) Clasificación LLM (opcional).
translation_scheduler.py — 0.5 CPU Crea filas traducciones pendientes.
remote_translator_worker.py NLLB-200 (600M/1.3B) GPU/CPU Cliente WebSocket → servidor /ws/worker; reconnect exponencial; procesa jobs de traducción. Este es el prototipo perfecto para el módulo "worker" de la app de escritorio.

Dependencia CTranslate2: libctranslate2.so requiere patchelf --clear-execstack en Linux (ver Dockerfile.remote-worker), y torch==2.1.0 cu118. Esto condiciona el empaquetado nativo (ver §8 y §11).

2.6 Base de Datos PostgreSQL

Esquema (fuente: init-db/00-complete-schema.sql + 37 migraciones numeradas; se aplica al iniciar el contenedor, sin framework de migraciones Go).

Tablas principales (28):

  • Núcleo: users, config, continentes, categorias, paises, feeds, fuentes_url, noticias, traducciones.
  • Enriquecimiento: tags, tags_noticia, entity_aliases, entity_images, topics, news_topics, related_noticias, eventos, eventos_noticias.
  • IA: traduccion_embeddings (array DOUBLE PRECISION[]), vista embeddings.
  • Social: favoritos, search_history, user_search_tags, alertas, videos, video_parrillas, remote_workers, translation_stats, feeds_pending.

Características PostgreSQL-específicas (problemas para un embebido SQLite):

  1. PK de noticias = VARCHAR(32) = MD5 hex de la URL.
  2. Full-text en español: columnas tsvector (tsv, search_vector_es) con triggers (to_tsvector('spanish', ...)) + índices GIN.
  3. traduccion_embeddings.embedding es un array de precisión doble.
  4. Consultas avanzadas: FOR UPDATE SKIP LOCKED (workers), unnest (bulk inserts), generate_series (mentions), CTEs con ventanas (alerta anomalías), ON CONFLICT DO NOTHING/UPDATE.
  5. El backend llama a pg_dump/psql desde cmd/admin.go para backup/restore (requiere el binario postgresql-client en la imagen).

Fuga de memoria / diferencia de tipos: News.ID en Go models es int64; los handlers devuelven el ID string real. Los clientes deben tratar los IDs de noticia como strings opacos.

2.7 Qdrant (Base Vectorial)

  • Colección única news_vectors (distancia coseno, dim derivada del modelo MiniLM = 384).
  • Payload: news_id, titulo, resumen, url, fuente, lang, fecha, category, pais.
  • El worker qdrant-worker hace upserts de puntos con UUIDs; marca traducciones.vectorized=TRUE y guarda qdrant_point_id.
  • ⚠️ La búsqueda semántica vía API (/api/search?semantic=true) es un stub vacío: no consulta Qdrant.

2.8 Redis

  • 512MB maxmemory, appendonly, requirepass.
  • Usado por ctranslator_worker como cache de traducciones (títulos/cuerpos).
  • El backend Go solo establece la conexión; no usa los helpers de cache.
  • Para la app nativa: Redis es irrelevante (el servidor lo gestiona).

2.9 Modelos ML y Pesos

Modelo Tamaño Uso
nllb-ct2 (600M, int8) ~600MB (models/) Traducción CPU
nllb-ct2-1.3b ~1.3GB Traducción GPU/calidad
nllb-200-distilled-600M (tokenizer) via HF Tokenizer
paraphrase-multilingual-MiniLM-L12-v2 ~470MB Embeddings
es_core_news_lg (spaCy) ~500MB NER
Mistral-7B (Ollama) ~4GB Categorización LLM (opcional)
hf_cache/ 7.9GB Caché HuggingFace

Conclusión para el dispositivo: los modelos completos no caben en móvil de forma realista. Ver §7.

2.10 Superficie de API Completa

Todas las rutas bajo /api, auth JWT Bearer (salvo las públicas).

Método Ruta Auth Descripción
GET /health No Health check
GET /api/wiki-images/* No Estáticos (imágenes Wikipedia)
POST /api/auth/login No Login
POST /api/auth/register No Registro (1er usuario = admin)
GET /api/auth/check-first-user No ¿Primer usuario?
GET /api/auth/me Sí Usuario actual
GET /api/news No Lista paginada con filtros q, category_id, country_id, translated_only, page, per_page
GET /api/news/:id No Detalle + entidades con wiki
DELETE /api/news/:id Sí Eliminar noticia
GET /api/feeds No Feeds paginados + categorías/países + conteos
GET /api/feeds/export No CSV
GET /api/feeds/:id No Detalle feed
POST /api/feeds Sí Crear feed
POST /api/feeds/import Sí Importar CSV
PUT /api/feeds/:id Sí Actualizar
DELETE /api/feeds/:id Sí Eliminar
POST /api/feeds/:id/toggle Sí Activar/desactivar
POST /api/feeds/:id/reactivate Sí Reset fallos
GET /api/search No Búsqueda texto (q, lang; semantic=true = stub)
GET /api/search/suggestions Sí Términos del usuario
POST /api/searchlog Sí Registrar búsqueda
GET /api/entities No Entidades canónicas paginadas
GET /api/entities/news No Noticias de una entidad
GET /api/entities/mentions No Serie temporal de menciones
GET /api/alerts No Alertas de anomalías
POST /api/alerts/:id/read Sí Marcar leída
POST /api/alerts/read-all Sí Marcar todas leídas
GET /api/stats No Estadísticas globales
GET /api/categories No Categorías
GET /api/countries No Países
POST /api/admin/aliases Admin Crear aliases
GET /api/admin/aliases/export Admin CSV
POST /api/admin/aliases/import Admin Importar CSV
POST /api/admin/entities/retype Admin Cambiar tipo entidad
GET /api/admin/backup Admin pg_dump SQL
GET /api/admin/backup/news Admin ZIP noticias
POST /api/admin/restore Admin Restaurar
GET /api/admin/users Admin Usuarios
POST /api/admin/users/:id/promote Admin Hacer admin
POST /api/admin/users/:id/demote Admin Quitar admin
POST /api/admin/reset-db Admin Vaciar 11 tablas
POST /api/admin/alerts/scan Admin Ejecutar escáner
GET /api/admin/workers/status Admin Estado Docker
GET /api/admin/workers/stats Admin Throughput traducción
POST /api/admin/workers/config Admin Config traducción
POST /api/admin/workers/start Admin docker compose up
POST /api/admin/workers/stop Admin docker compose stop
GET/POST /api/admin/workers/remote Admin Listar/crear workers remotos
GET/DELETE /api/admin/workers/remote/:id Admin Detalle/borrar
POST /api/admin/workers/remote/:id/toggle Admin Online/offline
POST /api/admin/workers/remote/:id/regenerate-key Admin Regenerar API key
GET /ws/worker API key WebSocket workers remotos

2.11 Configuración (Variables de Entorno)

Sin archivo .env en Go; todo via env vars inyectadas por Docker Compose. Tabla clave:

Variable Default Uso
SERVER_PORT 8080 Puerto API
DATABASE_URL postgres://rss:rss@localhost:5432/rss DSN API
REDIS_URL redis://localhost:6379 DSN cache
SECRET_KEY — Firma JWT
JWT_EXPIRATION 24h Leído pero ignorado
TRANSLATION_URL http://libretranslate:7790 Servicio traducción (sin uso real)
OLLAMA_URL http://ollama:11434 Embeddings/categorización
SPACY_URL http://spacy:8000 NER
DOCKER_COMPOSE_DIR /datos/rss2 Directorio para docker compose de admin
WIKI_IMAGES_PATH /app/data/wiki_images Imágenes wiki
ALLOWED_ORIGINS * CORS

Los 6 workers usan DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS + variables propias (SCRAPER_*, WIKI_*, TOPICS_*, RELATED_*, QDRANT_*, ALERTS_*).

2.12 Limitaciones Relevantes para la Migración

  1. SemanticSearch no implementado en la API Go (stub vacío).
  2. JWT_EXPIRATION ignorado — sin refresh tokens; crítico en móvil.
  3. Admin depende de Docker (start/stop/status/backup/restore hacen exec.Command).
  4. Sin framework de migraciones DB (todo en el init del contenedor PostgreSQL).
  5. Esquema fuertemente PostgreSQL-específico (tsvector, GIN, arrays, FOR UPDATE SKIP LOCKED).
  6. Sin tests en frontend y sin suite CI visible para la mayoría de servicios.
  7. Sin rate-limiting real (stub no conectado).
  8. Mensajes y UI 100% en español hardcodeados (falta i18n).
  9. Favoritos sin backend aunque existe la tabla favoritos.

3. Objetivos y Requisitos de la Nueva Aplicación

3.1 Requisitos Funcionales

  1. RF-1 Lectura de noticias: listar, filtrar (categoría, país, solo traducidas), ver detalle, entidades con Wikipedia.
  2. RF-2 Búsqueda: texto (y semántica si se implementa), sugerencias.
  3. RF-3 Favoritos: guardar/eliminar noticias (sincronizados con servidor).
  4. RF-4 Gestión de feeds: CRUD, activar/desactivar, reactivar, importar/exportar CSV.
  5. RF-5 Análisis: entidades populares, evolución temporal de menciones, alertas de anomalías.
  6. RF-6 Estadísticas globales.
  7. RF-7 Autenticación: login/registro, sesión persistente, logout.
  8. RF-8 Administración (solo admin): usuarios, aliases, workers, backup/restore, reset.
  9. RF-9 Notificaciones push (móvil) y del sistema (desktop) para nuevas alertas.
  10. RF-10 Modo offline: lectura de noticias cacheadas sin conexión.
  11. RF-11 (Avanzado) Modo worker: el desktop puede actuar como worker de traducción remota (WebSocket GPU/CPU) conectado a un servidor central.
  12. RF-12 (Avanzado) Núcleo local: el desktop puede ejecutar el backend completo embebido (servidor personal local) con la API en localhost.

3.2 Requisitos No Funcionales

# Requisito
RNF-1 Instalación nativa en Windows 10/11 x64, Linux x64 (deb/AppImage/Flatpak), Android 8+, iOS 15+.
RNF-2 Tamaño de descarga: ≤80MB (sin modelos embebidos).
RNF-3 Arranque < 3s; UI fluida (60fps) en dispositivos medios.
RNF-4 Cifrado de credenciales (Keychain/Keyring/Keystore).
RNF-5 Actualizaciones automáticas (auto-update).
RNF-6 Soporte offline-first con reconciliación al reconectar.
RNF-7 Cumplir políticas de tiendas (Google Play, App Store) — sin descargas de modelos gigantes en primer arranque móvil.
RNF-8 Seguridad: JWT con refresh, almacenamiento seguro, https obligatorio en producción.
RNF-9 i18n (al menos ES + EN) — aprovechar la migración para introducir react-i18next.

4. Arquitectura Objetivo (Alternativas)

4.1 Opción A — Cliente–Servidor con Offline-First (RECOMENDADA)

┌─────────────────────────────────────────────────────────────────────────┐
│                         SERVIDOR RSS2 (central o personal)             │
│   Docker Compose / binario nativo → ingesta, IA, DB, Qdrant, Redis     │
│   nginx :8888 → API REST /api + WebSocket /ws/worker                   │
└───────────────┬─────────────────────────────┬──────────────────────────┘
                │ HTTPS (REST + JWT refresh)  │ WebSocket (trabajos)
                ▼                             ▼
┌───────────────────────────────┐   ┌────────────────────────────────────┐
│  Apps nativas (clientes)      │   │  Desktop con GPU (modo worker)    │
│  Windows / Linux / Android/iOS│   │  NLLB-200 local via WebSocket     │
│                               │   │  (reutiliza remote_translator)    │
│  SQLite local (caché offline) │   └────────────────────────────────────┘
└───────────────────────────────┘

Ventajas: reutilización total del backend actual (sin reescribir nada de Go/Python); las apps son "clientes ricos"; el modo worker desktop aprovecha la infraestructura WebSocket existente. Desventajas: requiere conexión para ingesta en tiempo real; el modo offline es de lectura/caché.

4.2 Opción B — Núcleo Embebido Total (server local en el dispositivo)

Cada desktop ejecuta todo el stack embebido: backend Go + PostgreSQL embebido + Qdrant + modelos ML. Móviles se conectan al desktop o usan el servidor central.

Ventajas: privacidad total, sin servidor externo. Desventajas: enorme complejidad de empaquetado (Postgres/Qdrant/modelos por plataforma), requisitos de RAM/almacenamiento prohibitivos en móvil, y duplicación de todo el coste de operación. Descartado como objetivo principal, solo viable como modo avanzado desktop en Fase 5.

4.3 Opción C — Híbrida (recomendada como hoja de ruta de 3 etapas)

  1. Etapa 1 (MVP): Opción A pura — apps nativas como clientes del servidor central.
  2. Etapa 2 (Offline): Caché SQLite local + sincronización bidireccional + push.
  3. Etapa 3 (Pro): Modo "worker" desktop (GPU) + modo "núcleo local" desktop (backend embebido) para usuarios avanzados y despliegues domésticos.

Recomendación: Opción C. Es la que equilibra esfuerzo, valor y viabilidad técnica. El resto del plan se basa en ella.


5. Selección Tecnológica por Plataforma

5.1 Estrategia de Reuso del Frontend

El 95% de la UI actual es React + Tailwind. Se recomienda una sola base de código React empaquetada con:

Plataforma Tecnología Razonamiento
Windows + Linux Tauri 2 (Rust shell + WebView2/WebKitGTK) Binarios pequeños (~8-15MB), bajo consumo, API nativa para archivos/notificaciones/servicios, ideal para empaquetar el backend Go como sidecar.
Android + iOS Capacitor 6 (wrapper nativo de la misma SPA React) Reutiliza la SPA tal cual; acceso a Push, SecureStorage, Filesystem, SplashScreen, autoupdate (capgo).

Alternativa evaluada y rechazada:

  • Electron: gran ecosistema y autoupdate maduro, pero binarios de 80-120MB y alto consumo RAM; innecesario con WebView del SO moderno. (Puede elegirse si el equipo prefiere JS puro para el shell — coste en tamaño/RAM.)
  • Flutter / React Native: supondría reescribir las ~4.300 líneas TSX. Solo tiene sentido si se quiere UI 100% nativa y se descarta el reuso. No recomendado dado el frontend existente.
  • .NET MAUI / Kotlin Multiplatform: mismo argumento, coste de reescritura alto.

Arquitectura de paquetes (monorepo):

rss2/
├── app/                          # NUEVO: aplicación multiplataforma
│   ├── packages/
│   │   ├── core-ui/              # Componentes React reutilizables (portados)
│   │   ├── api-client/           # Cliente HTTP + tipos (portados de api.ts)
│   │   ├── storage/              # Abstracción de almacenamiento seguro
│   │   ├── sync/                 # Motor de sincronización offline
│   │   └── platform/             # Bridge nativo (Tauri/Capacitor)
│   ├── src/                      # SPA React compartida
│   ├── desktop/                  # Shell Tauri (Rust)
│   │   └── src-tauri/
│   ├── mobile/                   # Configuración Capacitor (Android/iOS)
│   └── worker/                   # Módulo "worker de traducción" embebido
│       └── (port de remote_translator_worker.py a Rust/embedded)

5.2 Desktop (Windows/Linux) — Tauri 2

  • Rust shell con tauri-plugin-shell, tauri-plugin-store, tauri-plugin-notification, tauri-plugin-autostart, tauri-plugin-updater (v2), tauri-plugin-sql (SQLite vía rusqlite), tauri-plugin-fs.
  • Comunicación JS↔Rust: comandos invoke (ej. save_dialog, read_file, notification_permission).
  • Sidecar backend Go: compilado con go build -ldflags "-s -w" y registrado como sidecar binario de Tauri (externalBin). Arrancado/bajo control de Rust (tauri-plugin-shell), con auto-restart y registro de logs.
  • Ventana: aprovecha WebView2 (Windows 10/11 preinstalado) y WebKitGTK (Linux). Tamaño mínimo 1024×700, redimensionable.
  • Bandeja del sistema (tray) para background-ingesta (opcional en Fase 5).
  • Notificaciones nativas del sistema.

5.3 Android/iOS — Capacitor 6

  • Mismo dist/ de Vite sirve para web y para Capacitor (npx cap add android/ios).
  • Plugins requeridos:
    • @capacitor/preferences (o @capacitor/secure-storage-plugin) → sustituye localStorage para token.
    • @capacitor/push-notifications → notificaciones push (FCM/APNs).
    • @capacitor/filesystem → exportación de CSV/ZIP/SQL a la carpeta de descargas / Share.
    • @capacitor/share → compartir noticias.
    • @capacitor/network → detección de conectividad para el modo offline.
    • @capacitor/status-bar, @capacitor/splash-screen.
  • Requiere HashRouter en móvil (sin window.history en file://).
  • Actualizaciones: Capgo o re-publicación en tiendas para updates OTA; revisión obligatoria para iOS.

5.4 Servidor (cerebro)

Dos opciones de despliegue del backend que la app debe soportar:

  1. Servidor central (SaaS/self-hosted): el docker-compose.yml actual sin cambios; la app se conecta a una URL HTTPS.
  2. Núcleo embebido desktop (Fase 5): el desktop ejecuta un binario único que integra la API Go + los workers esenciales (ingesta, wiki, topics, langdetect) sobre PostgreSQL embebido portable (ver §8), sin Docker.

5.5 Base de Datos Local — SQLite SOLO como caché de cliente (nunca como almacén principal)

Decisión arquitectónica clave (escala: millones de noticias): el sistema de registro (system of record) es PostgreSQL + Qdrant en el servidor, igual que hoy. SQLite se usa ÚNICAMENTE como caché acotada y estado de usuario en el dispositivo — nunca debe contener el dataset completo ni servir como almacén principal.

Razones técnicas (derivadas del código actual):

Limitación de SQLite Impacto en RSS2 a escala de millones
Escritura serializada (single-writer) El rss-ingestor-go corre hasta 100 workers concurrentes y los workers Python usan FOR UPDATE SKIP LOCKED. SQLite encolaría todas las escrituras → cuello de botella de ingesta.
Sin FOR UPDATE SKIP LOCKED ni unnest Los workers de traducción/topics dependen de estos patrones; reimplementarlos sobre SQLite degrada el rendimiento y añade bugs.
FTS5 vs tsvector/GIN El esquema actual usa to_tsvector('spanish', ...) con triggers e índices GIN. FTS5 funciona, pero con millones de filas y escrituras concurrentes el mantenimiento del índice es más débil que Postgres.
Embeddings y vectores traduccion_embeddings guarda arrays DOUBLE PRECISION[] (384 dims ≈ 3KB+/fila). 1 noticia → N traducciones → M embeddings: millones de noticias ⇒ decenas de millones de filas. La búsqueda semántica necesita Qdrant, no similitud in-memory de SQLite.
Backups/restore El backend ya usa pg_dump/psql; con SQLite habría que reimplementar y perderías la madurez de esas herramientas.

Dónde SÍ es válido SQLite (roles de cliente):

  • Caché de lectura con evicción forzada (ver §6.1: presupuesto máx. ~50k noticias y retención de 7–30 días, LRU).
  • Estado de usuario: favoritos, leídos, outbox de escrituras pendientes, metadatos de feeds.
  • Esquema local (ver §6.2) no replica el esquema PostgreSQL: es un esquema de aplicación mínimo para lectura rápida.

Alternativa evaluada: libSQL/Turso (replicación distribuida) — solo si más adelante se quiere multi-dispositivo con el cliente como fuente. No recomendado ahora; el servidor PostgreSQL sigue siendo la fuente de verdad.

5.6 Resumen de Decisión

Componente Tecnología Reuso del código actual
UI React 18 + Tailwind (misma SPA) 95% reutilizado
Desktop shell Tauri 2 (Rust) —
Móvil shell Capacitor 6 —
Cliente HTTP Axios (portado a api-client) 100%
Almacenamiento seguro Keyring/Keychain/Keystore nuevo
DB local SQLite nuevo
Motor de traducción (worker) CTranslate2 (embedded, port a Rust opcional) reutiliza lógica de remote_translator_worker.py
Servidor Docker Compose actual / binario nativo 100%

6. Estrategia de Datos y Sincronización

6.1 Modelo Offline-First

Principios:

  1. Lectura: todas las listas de noticias/entidades/feeds se cachean en SQLite local con presupuesto y evicción forzada. SQLite es caché, no almacén: la app NUNCA guarda el dataset completo.
  2. Presupuesto de caché (regla dura): máx. 50.000 noticias en news_cache y retención por TTL (5 min listas, 24h detalle, 30 días fondo). Al superar el tope se aplica LRU (borrar por fetched_at más antiguo) y se recorta por antigüedad (fecha < now - 30d). El servidor conserva el histórico completo; la app solo su ventana de trabajo.
  3. Escritura (favoritos/leídos): se aplican localmente al instante y se encolan en una tabla outbox para sincronizar. El outbox se vacía tras confirmación del servidor y no tiene límite práctico (son pocas filas por usuario).
  4. Sincronización: un motor de sync (background worker) reconcilia outbox contra el servidor cuando hay red; conflictos resueltos por "última escritura gana" con timestamp.
  5. Rehidratación: al reconectar, se invalida la caché y se refresca desde el servidor.

6.2 Esquema SQLite Local (caché + estado, con evicción)

-- Caché de lectura (SIEMPRE acotada: máx. ~50k filas, LRU + TTL)
CREATE TABLE news_cache (
  id TEXT PRIMARY KEY,            -- MD5 de la URL (string opaco)
  payload TEXT NOT NULL,          -- JSON completo de la noticia
  fetched_at INTEGER NOT NULL,    -- epoch ms (usado por el LRU)
  ttl INTEGER NOT NULL DEFAULT 86400000
);
CREATE TABLE list_cache (
  key TEXT PRIMARY KEY,           -- "news:page=1:cat=2" etc.
  payload TEXT NOT NULL,
  fetched_at INTEGER NOT NULL,
  ttl INTEGER NOT NULL DEFAULT 300000
);
CREATE INDEX idx_news_fetched ON news_cache(fetched_at);
-- Trabajo de evicción (job diario):
--   DELETE FROM news_cache WHERE fetched_at < now - 30d;
--   DELETE FROM news_cache WHERE id IN (
--     SELECT id FROM news_cache ORDER BY fetched_at ASC
--     LIMIT (SELECT MAX(0, COUNT(*) - 50000) FROM news_cache));
--   DELETE FROM list_cache WHERE fetched_at < now - ttl;

-- Estado de usuario (sync bidireccional)
CREATE TABLE favorites (
  noticia_id TEXT PRIMARY KEY,
  created_at INTEGER NOT NULL,
  dirty INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE read_states (
  noticia_id TEXT PRIMARY KEY,
  read_at INTEGER NOT NULL,
  dirty INTEGER NOT NULL DEFAULT 1
);

-- Cola de escrituras pendientes de sincronizar (pocas filas; se vacía al sync)
CREATE TABLE outbox (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  op TEXT NOT NULL,               -- 'add_favorite' | 'remove_favorite' | 'read'
  payload TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  synced INTEGER NOT NULL DEFAULT 0
);

-- Metadatos de feeds (para gestión offline)
CREATE TABLE feeds_meta (
  id INTEGER PRIMARY KEY,
  nombre TEXT, url TEXT, activo INTEGER,
  last_error TEXT, updated_at INTEGER
);

-- Config local
CREATE TABLE app_settings (key TEXT PRIMARY KEY, value TEXT);

Nota de escala: este esquema NO es una réplica del esquema PostgreSQL (que alberga los millones de noticias, traducciones, embeddings y relaciones). Es una capa de lectura/escritura mínima cuyo tamaño total se mantiene < 100MB gracias a la evicción.

6.3 Sincronización Bidireccional

Flujo del motor de sync:

  1. Detectar conectividad (@capacitor/network / evento de red en Tauri).
  2. Enviar outbox pendiente en lotes (con el JWT).
  3. Aplicar confirmaciones (borrar filas sincronizadas).
  4. Pull incremental: GET /api/news?since=<cursor> (requiere añadir filtro since al backend, ver §9) → upsert en news_cache.
  5. Refrescar favorites del servidor (una vez POST /api/favorites esté implementado, ver §6.5).

6.4 Imágenes y Multimedia

  • Las imágenes de noticias son URLs remotas; se cachean en disco (~/.cache/rss2 en desktop, app dir en móvil) con límite (ej. 200MB LRU).
  • Las imágenes wiki se sirven desde /api/wiki-images/; la app debe descargarlas y cachearlas igual.
  • Lazy-loading ya presente en las tarjetas; optimizar con srcset/formatos modernos.
  • En móvil: preferir miniatura de 400px para ahorrar datos (sugerir añadir parámetro de tamaño al backend).

6.5 Favoritos: de localStorage a sincronizados

El frontend actual guarda favoritos en localStorage (objetos completos). Para offline-first real:

  1. Añadir endpoints al backend: GET/POST/DELETE /api/favorites sobre la tabla favoritos existente (no utilizada por la UI actual).
  2. La app usa la tabla local favorites + outbox.
  3. Migrar los favoritos existentes de localStorage al SQLite local en el primer arranque de la app.

6.6 Backup y Exportación Local

  • Desktop: exportar favoritos/caché a archivo (botón nativo save dialog).
  • Sustituir URL.createObjectURL+<a download> por API nativa:
    • Tauri: invoke('save_dialog') + fs.write_file.
    • Capacitor: Filesystem.writeFile a DIRECTORY.DOCUMENTS + Share.share con el URI.
  • Backup servidor (/admin/backup) sigue disponible solo en modo admin.

7. Estrategia de IA/ML en el Dispositivo

7.1 Realidad de los Modelos

Modelo Tamaño Desktop (x64) Android/iOS
NLLB-200 600M (CT2 int8) ~600MB ✅ viable (2-4 GB RAM) ❌ excesivo para la mayoría
NLLB-200 1.3B (float16) ~2.6GB ✅ con GPU 8GB+ ❌
MiniLM embeddings ~470MB ✅ ⚠️ borde
spaCy es_core_news_lg ~500MB ✅ ❌
Mistral-7B ~4GB ⚠️ solo con GPU ❌

7.2 Modelo por Capas (tiered)

Capa 0 (obligatoria, sin IA local): la app solo consume la API. Toda la IA corre en el servidor. Es el modo por defecto para móvil.

Capa 1 (desktop con GPU — modo worker): el desktop instala (bajo demanda, descarga de HuggingFace con opción de "descargar ahora/diferir") el modelo NLLB CTranslate2. Se conecta al servidor por WebSocket (/ws/worker) como worker de traducción remota. Implementación: portar remote_translator_worker.py a un servicio interno de la app.

Opciones de implementación del worker desktop:

  • A) Embed Python: empaquetar el intérprete Python (PyInstaller) con ctranslate2+transformers como proceso sidecar de la app Tauri. Rápido de portar (reutiliza el .py tal cual) pero binarios grandes (300-500MB) y fragilidad de CTranslate2 en empaquetado.
  • B) Port a Rust ctranslate2-rs: binario único pequeño, sin Python. Más trabajo pero mucho más robusto y limpio. Recomendado a medio plazo.
  • C) Go + cgo a CTranslate2 C API: intermedio.

Recomendación: empezar con (A) (time-to-market) y migrar a (B) como mejora de Fase 5.

7.3 IA On-Device Móvil (opcional, futuro)

Si se desea búsqueda semántica o traducción offline en móvil:

  • Traducción: modelos ONNX/ML Kit (NLLB no es práctico; alternativas: google ML Kit translate offline ~80 idiomas, pesos menores).
  • Embeddings: ONNX Runtime Mobile con MiniLM cuantizado (int8, ~40MB).
  • NER: string matcher local (como el worker topics de Go) en lugar de spaCy.

Estos se descargan bajo demanda con consentimiento del usuario (política de tiendas).

7.4 Categorización LLM y Búsqueda Semántica

  • Categorización LLM (Ollama/Mistral): permanece en servidor; la app solo muestra resultados.
  • Búsqueda semántica: el stub de la API deberá implementarse en el servidor consultando Qdrant (Fase 1 de servidor, independiente de la app). La app simplemente usará el endpoint cuando exista.

8. Núcleo Embebido (Embedded Backend)

8.1 Contexto

Para la Fase 5 (modo avanzado desktop y despliegues domésticos), se propone ejecutar el backend sin Docker: un binario único que integre API + workers sobre un almacén local.

Decisión de escala: dado que el sistema acumulará millones de noticias, el almacén del núcleo embebido debe ser PostgreSQL embebido, NO SQLite. PostgreSQL embebido mantiene paridad total con el servidor (mismo esquema, mismos workers, FOR UPDATE SKIP LOCKED, tsvector/GIN, arrays, pg_dump). SQLite queda exclusivamente como caché de cliente (§6) y solo se ofrece como "modo limitado" para despliegues personales pequeños con techo documentado (§8.5).

8.2 Estrategia de Almacén del Núcleo Embebido

Opción A (RECOMENDADA): PostgreSQL embebido.

Aspecto Detalle
Librería github.com/fergusstrange/embedded-postgres (descarga/arranca un binario portable de Postgres) o zonky/otj-pg-embedded.
Compatibilidad 100% con el esquema init-db/*.sql actual; mismo DATABASE_URL; los 6 workers Go y los handlers no cambian.
Backup/restore Se reutilizan pg_dump/psql ya implementados en internal/handlers/admin.go.
Concurrencia Soporta los 100 workers del ingestor y FOR UPDATE SKIP LOCKED de traducción.
Coste de empaquetado +~50–80MB de binarios Postgres por SO (no es pesado para desktop).
Escala Capaz de albergar millones de noticias sin cambio de diseño.

El binario de Postgres se registra como sidecar adicional de Tauri (junto al núcleo Go) y Rust lo arranca/supervisa antes de lanzar la API. Datos en el directorio de la app (%APPDATA%/rss2/pgdata). Puerto loopback (5432 → uno efímero) y credenciales aleatorias generadas en el primer arranque.

Opción B: "Modo limitado" SQLite (solo personal, con techo).

  • Válido únicamente para el perfil "usuario doméstico con feeds personales".
  • Techo duro: ≤ 200.000 noticias, ≤ 5 feeds activos, sin búsqueda semántica (solo FTS5 de texto), sin clustering/related.
  • Debe existir ruta de migración al superar el techo: exportar a pg_dump/archivo SQL e importar en un servidor PostgreSQL o en el modo A. Documentar en la UI ("Tu catálogo local ha superado el modo limitado").
  • Riesgo asumido: reescritura de consultas Postgres-específicas (unnest, FOR UPDATE SKIP LOCKED, generate_series) para SQLite.

Recomendación: implementar solo la Opción A en la Fase 5. Mantener SQLite únicamente como caché de cliente. Si el equipo quiere abaratar el modo personal, la Opción B puede añadirse después como mejora, nunca como base.

8.3 Componentes del Binario Único

rss2-core (un solo binario Go)
├── API REST (Gin, puerto 127.0.0.1:8080) — rutas /api completas para la app
├── Motor PostgreSQL embebido (sidecar portable, arrancado por Rust) o Postgres externo
├── Módulo de ingesta RSS (port del rss-ingestor-go) — goroutine con ticker
├── Módulo wiki (port de wiki_worker) — goroutine
├── Módulo topics (port de topics) — goroutine
├── Módulo langdetect (Go) — goroutine
├── Módulo NER ligero (string matcher) — goroutine
└── Qdrant embebido (sidecar, opcional) o búsqueda vectorial desactivada

Configuración: archivo rss2.json en el directorio de configuración del SO (%APPDATA%/rss2, ~/.config/rss2, ~/Library/Application Support/rss2). Generado en el primer arranque con secretos aleatorios.

8.4 Gestión de Procesos en la App

  • Arranque: Tauri sidecar arranca el binario con --serve y espera el healthcheck (GET /health).
  • Supervisión: Rust monitoriza el proceso; auto-restart tras crash (máx. 3 reintentos, backoff exponencial).
  • Shutdown ordenado: señal SIGTERM → shutdown graceful del servidor Gin.
  • Logs: stderr redirigido a rss2.log rotativo.
  • Seguridad: bind solo a 127.0.0.1; token de sesión local; CORS restringido a tauri://localhost y https://app.rss2.local.

8.5 Descarga del Núcleo (Opcional en Desktop)

El paquete del núcleo (binario Go ≈30–50MB + binarios PostgreSQL portable ≈50–80MB) puede descargarse bajo demanda desde el servidor de releases (GitHub Releases) con checksum SHA-256 verificado, e instalarse en el directorio de la app. Si el usuario no descarga el núcleo, la app funciona en modo cliente puro (Capa 0).


9. Rediseño de la API para Clientes Nativos

Cambios mínimos necesarios en el backend Go para soportar clientes nativos de forma robusta.

9.1 Autenticación con Refresh Tokens

El JWT actual expira en 24h fijas y la app móvil no puede pedir re-login cada día.

  1. Añadir tabla refresh_tokens (id, user_id, token_hash, expires_at, revoked_at, device_info, created_at).
  2. Nuevos endpoints:
    • POST /api/auth/refresh (con refresh token → nuevo par).
    • POST /api/auth/logout (revoca refresh).
  3. Usar JWT_EXPIRATION config (arreglar el hardcode de internal/auth/jwt.go).
  4. La app guarda el refresh token en SecureStorage/Keychain y lo renueva de forma transparente.

9.2 Endpoints Nuevos Requeridos

Endpoint Propósito
GET/POST/DELETE /api/favorites Sincronización de favoritos (tabla favoritos ya existe)
GET /api/news?since=<ISO> Pull incremental para sync offline
GET /api/news/feed (opcional) Feed agregado optimizado para móvil (paginado con cursor)
GET /api/push/config + POST /api/push/register Registrar token FCM/APNs por usuario
POST /api/alerts/:id/read (ya existe) —
GET /api/version Informa versión de API para compatibilidad de la app

9.3 Notificaciones Push

  • Servidor: nuevo endpoint para registrar token de push; worker Go que consulta alertas nuevas y dispara notificaciones.
  • Android: FCM (firebase-messaging); iOS: APNs.
  • Desktop: notificaciones del sistema vía plugin Tauri (sin infraestructura push; eventos en tiempo real por WebSocket/polling ligero).
  • Canales: "Nuevas alertas", "Nueva traducción disponible" (opcional), "Estado de workers" (opcional, solo admin).

9.4 WebSocket para Tiempo Real

  • Actualmente solo /ws/worker (workers). Añadir un canal /ws/client opcional con auth JWT para: notificación de nuevas noticias, actualización de alertas en vivo, estado de traducción. La app puede degradar a polling cada 60s si el WebSocket no está disponible.

9.5 Versionado de API

  • Montar la API bajo /api/v1 (manteniendo /api como alias por compatibilidad web).
  • La app envía Accept-Version: 1 y User-Agent: rss2-desktop/1.4.2 para trazabilidad y rate-limiting diferenciado.

9.6 CORS y Seguridad

  • La app nativa no está sujeta a CORS (sin origin de navegador), pero Tauri WebView sí lo está: incluir tauri://localhost, http://localhost:* y https://*.rss2.local en ALLOWED_ORIGINS.
  • HTTPS obligatorio para conexión a servidores remotos (la app rechaza http:// salvo localhost).
  • Certificado pinning opcional en móvil para servidores propios.

9.7 Rediseño de Funciones Admin Dependientes de Docker

Los endpoints /admin/workers/start|stop|status ejecutan docker compose. Para la app:

  • Modo cliente: mostrar solo estado reportado por el servidor; ocultar controles de contenedores en móvil.
  • Modo núcleo embebido: los controles mapean a APIs de gestión del proceso interno (no Docker).

10. Migración del Frontend Paso a Paso

10.1 Fase 0 — Refactor de base (sin cambios visuales)

  1. Extraer api-client: mover services/api.ts a un paquete independiente con inyección de baseURL, transporte y almacenamiento.
  2. Crear storage.ts (abstracción): interfaz SecureStore { get, set, remove } con implementaciones: localStorage (web/dev), Keyring/tauri-plugin-store (desktop), Preferences+SecureStorage (móvil).
  3. Sustituir localStorage (token/user/favorites) por la abstracción.
  4. Unificar fetch: envolver las páginas que usan api crudo y useEffect para que usen apiService + React Query (Populares, Alertas, Analisis, Admin*).
  5. Arreglar deudas menores: btn-danger, filtros muertos de Search, Account persistente, limpiar imports muertos (Favorites, clsx), implementar cn().
  6. Añadir i18n: react-i18next con diccionarios ES/EN; marcar todos los strings.
  7. Introducir router portable: pasar de BrowserRouter a un wrapper que use BrowserRouter en web y HashRouter en móvil (detectable por variable de compilación).

10.2 Fase 1 — Abstracción de APIs de navegador

Función Abstracción (platform/) Desktop (Tauri) Móvil (Capacitor)
Descarga de archivos saveFile(name, bytes) invoke('save_dialog') + fs Filesystem.writeFile + Share
Diálogos confirmDialog(msg) invoke('confirm') (Rust nativo) modales React (no window.confirm)
Notificación notify(title, body) plugin notification plugin push/local
Conectividad onNetworkChange(cb) eventos Rust plugin network
Fecha/hora date-fns (ya portable) — —
Apertura de enlaces openExternal(url) plugin opener Browser.open (in-app browser)

10.3 Fase 2 — Adaptaciones móviles

  1. Nav responsive: sustituir hidden sm:flex por un componente de navegación con hamburguesa / bottom-tab en móvil.
  2. WikiTooltip táctil: añadir soporte de tap (primer tap muestra, segundo navega) — reemplazar hover-only.
  3. Tablas (Feeds, Alertas, Admin): en móvil, convertir a listas de tarjetas en vez de overflow-x-auto.
  4. LineChart: min-w-[560px] → rescalar a viewBox responsivo (eliminar scroll horizontal).
  5. Safe areas: padding con env(safe-area-inset-*).
  6. Pull-to-refresh en listas (react-query + plugin de refresco nativo).
  7. Infinite scroll / paginación para listas de noticias en lugar de paginación numérica.

10.4 Fase 3 — Offline y Sincronización

  1. Integrar storage/SQLite local (tauri-plugin-sql, @capacitor-community/sqlite).
  2. Implementar el motor de sync del §6.3.
  3. Convertir las consultas a React Query para servir primero de caché local (placeholderData/initialData del SQLite) y refrescar del servidor.
  4. Estado de conexión: banner "Modo offline" + cola de acciones pendientes.

10.5 Fase 4 — Shells Nativos

Tauri (desktop):

  • npm create tauri-app + config en app/desktop/src-tauri.
  • Plugins: shell (sidecar), sql, store, notification, autostart, updater.
  • tauri.conf.json: externalBin → binaries/rss2-core-*, ventana 1024×700, capacidades/perms restringidas.
  • Build matrix (ver §12).

Capacitor (móvil):

  • npx cap init + npx cap add android && npx cap add ios.
  • Plugins listados en §5.3.
  • capacitor.config.ts: webDir: dist, androidScheme: https.
  • Firma de APK/AAB con keystore; provisioning para iOS.

10.6 Gestión de Sesión en la App

  • Flujo: login → guardar access token (memoria + SecureStorage) y refresh token (SecureStorage).
  • Interceptor: 401 → intentar refresh → reenviar petición → si falla, logout.
  • Almacenar user (JSON) en SecureStorage y sincronizar estado de sesión por eventos internos (en vez de storage event del navegador).

11. Empaquetado y Distribución por Plataforma

11.1 Windows

  • Formato: NSIS installer (.exe) vía Tauri bundler; MSI opcional (WiX).
  • Firma: certificado Authenticode (EV recomendado) con signtool en CI (Secret: WINDOWS_CERT_BASE64, WINDOWS_CERT_PASSWORD).
  • SmartScreen: firmar y/o añadir reputación; la primera versión no firmada mostrará aviso.
  • Actualizaciones: tauri-plugin-updater con endpoint de manifest (GitHub Releases).
  • WebView2: Runtime instalado por defecto en Win10/11; incluir fallback de instalación silenciosa.
  • Instalación por usuario: sin privilegios admin (per-user), o instalador system-wide opcional.

11.2 Linux

  • Formatos: .deb (Debian/Ubuntu), .rpm (Fedora), AppImage (universal, recomendado para testing), Flatpak (distribución a largo plazo), .tar.gz portable.
  • WebKitGTK: dependencia webkit2gtk-4.1 + librsvg; documentar en README del paquete.
  • Firma: GPG para repos; Flatpak vía Flathub (si se desea).
  • Actualizaciones: AppImageUpdate (fork de AppImage) o el updater de Tauri con endpoint propio.

11.3 Android

  • Formato: AAB (App Bundle) para Google Play; APK firmado para distribución directa.
  • Firma: keystore app-release.keystore (guardar en CI como secret; nunca en git).
  • minSdk: 24 (Android 7.0+) recomendado por Capacitor 6 (minSdk 22+).
  • Play Console: política de "descarga de modelos on-demand" documentada; contenido de noticias = "News" (requiere moderación de contenido si es social).
  • Permisos: INTERNET, POST_NOTIFICATIONS (Android 13+), VIBRATE.
  • Distribución alternativa: F-Droid / directa (APK en el sitio).

11.4 iOS

  • Formato: .ipa vía App Store Connect (requiere Mac + Xcode + cuenta Apple Developer $99/año).
  • Firma: certificado Apple Distribution + provisioning profile (secrets en CI macOS).
  • App Store: política de contenido de noticias; pantalla de "primeros pasos"; descarga de modelos bajo demanda.
  • Notarización de macOS (si en el futuro se soporta macOS) — fuera de alcance inicial (solo Win/Linux desktop).
  • Actualización: OTA solo vía App Store (Capgo no funciona con el App Store para updates; requeriría MDM/supervisión). Descartar OTA en iOS.

11.5 Política de Actualizaciones

Plataforma Mecanismo
Windows tauri-plugin-updater (manifest JSON en GitHub Releases)
Linux tauri-plugin-updater (AppImage) / repos apt/Flatpak
Android Play Store + directa (APK) + opcional Capgo
iOS App Store (única vía)

11.6 Tamaños Estimados del Instalador

Plataforma Sin núcleo Con núcleo embebido Con worker Python (GPU)
Windows ~25MB ~60MB ~400MB
Linux ~25MB ~60MB ~400MB
Android ~15MB — —
iOS ~15MB — —

12. Infraestructura de Build y CI/CD

12.1 Pipeline GitHub Actions (matrix)

name: build-app
on: [push, pull_request, workflow_dispatch]

jobs:
  web:
    runs-on: ubuntu-latest
    steps:
      - checkout
      - setup-node, npm ci
      - npm run build            # tsc + vite build (dist/)
      - npm run test             # vitest (nuevos tests)
  desktop-windows:
    needs: web
    runs-on: windows-latest
    steps:
      - npm ci
      - cargo build --release (Tauri)
      - tauri build --bundles nsis
      - firma (signtool) + upload artifact
  desktop-linux:
    needs: web
    runs-on: ubuntu-latest
    steps:
      - apt: libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev
      - tauri build --bundles deb,appimage
      - upload artifact
  android:
    needs: web
    runs-on: ubuntu-latest
    steps:
      - java 17, Android SDK
      - npx cap sync android
      - gradlew assembleRelease (AAB + APK)
      - upload artifact
  ios:
    needs: web
    runs-on: macos-14
    steps:
      - npx cap sync ios
      - xcodebuild archive + export
      - (opcional) upload to TestFlight via App Store Connect API
  backend-core:
    runs-on: matrix (windows, ubuntu)
    steps:
      - go build -o rss2-core-{os}-{arch} ./cmd/rss2-core  # nuevo binario
      - upload artifact
  release:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: [desktop-windows, desktop-linux, android, backend-core]
    steps:
      - download artifacts
      - create GitHub Release con checksums (sha256sum)
      - publish manifest.json del updater

12.2 Versionado

  • Semver estricto (1.4.2). El package.json (app) y el manifiesto del updater comparten versión.
  • Tag v* dispara la release. main dispara builds de desarrollo (artifacts sin firmar).

12.3 Secretos en CI

Secret Uso
WINDOWS_CERT_BASE64 / WINDOWS_CERT_PASSWORD Firma Authenticode
ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_PASSWORD / ANDROID_KEY_ALIAS Firma AAB/APK
APPLE_DIST_CERT_BASE64 / APPLE_PROVISIONING_BASE64 Firma iOS
GH_TOKEN Publicar releases + manifest
FCM_SERVER_KEY / APNS_KEY Push (si se testea desde CI)

13. Plan de Implementación por Fases

Fase 0 — Preparación y Refactor (2–3 semanas)

Objetivo: base de código portable sin cambios de comportamiento.

  • Extraer api-client y storage como paquetes.
  • Sustituir localStorage por SecureStore.
  • Unificar fetch (apiService + React Query) en todas las páginas.
  • Arreglar deudas técnicas §2.3 (btn-danger, Search, Account, Favorites, i18n, router portable).
  • Añadir tests base (vitest + Testing Library) para api-client, storage y componentes clave.
  • Entregable: npm run build + npm test verde; la web sigue funcionando igual en Docker.

Fase 1 — Backend: Refresh Tokens + API para móvil (2 semanas)

  • Tabla refresh_tokens + endpoints /auth/refresh, /auth/logout.
  • Arreglar JWT_EXPIRATION en internal/auth/jwt.go.
  • Endpoints /api/favorites (GET/POST/DELETE).
  • GET /api/news?since= (pull incremental).
  • /api/version.
  • Implementar SemanticSearch real contra Qdrant (opcional pero recomendado).
  • Tests Go para los nuevos endpoints (patrón existente en internal/handlers/auth_test.go).
  • Entregable: API versionada y consumible por clientes nativos.

Fase 2 — Shell Desktop Tauri (Windows/Linux) — MVP (3–4 semanas)

  • Proyecto Tauri + integración del dist/ de Vite.
  • Plugins: store, notification, opener, updater.
  • Abstracciones platform/ (saveFile, confirmDialog, notify).
  • Login/registro + sesión persistente con refresh.
  • Pantallas principales: Home, News, Search, Favorites, Feeds (read-only), Stats.
  • Instaladores NSIS (Win) y deb/AppImage (Linux).
  • Entregable: instalador de escritorio funcional conectado a un servidor central.

Fase 3 — Shell Móvil Capacitor (Android/iOS) (4–5 semanas)

  • Proyecto Capacitor + sync de la SPA.
  • Plugins: preferences, secure-storage, filesystem, share, network, push, splash, status-bar.
  • HashRouter + adaptaciones móviles §10.3 (nav, tooltips, tablas, chart).
  • Push notifications (FCM/APNs) + endpoint /push/register.
  • Builds Android (AAB) e iOS (ipa) en CI.
  • Entregable: apps en Android e iOS instalables (TestFlight/Play internal testing).

Fase 4 — Offline-First y Sincronización (3–4 semanas)

  • SQLite local (desktop + móvil) con esquema §6.2.
  • Motor de sync (outbox + pull incremental) §6.3.
  • Favoritos sincronizados (backend + app).
  • Caché de imágenes con límite LRU.
  • Modo offline de lectura + banner de estado.
  • Pull-to-refresh e infinite scroll.
  • Entregable: app funcional offline con reconciliación al reconectar.

Fase 5 — Núcleo Embebido y Modo Worker (5–8 semanas)

  • Binario rss2-core (Go) + PostgreSQL embebido portable (Opción A) con el esquema init-db/*.sql intacto. Descartado SQLite como almacén principal (escala de millones); SQLite queda solo como caché de cliente.
  • Sidecar Postgres (arranque/supervisión/shutdown por Rust) + sidecar del núcleo.
  • Port de ingesta RSS, wiki, topics, langdetect al núcleo.
  • Supervisión de procesos: auto-restart, logs rotativos, healthcheck.
  • Configuración local rss2.json + wizard de "Configurar servidor local".
  • Descarga bajo demanda del núcleo y de Postgres portable (checksum verificado).
  • Modo worker: port de remote_translator_worker.py (opción A: PyInstaller sidecar; mejora B: Rust).
  • Gestión de workers remotos en la UI (crear API key, ver estado) — ya existe en AdminWorkers.
  • Entregable: desktop autónomo (sin Docker, a escala de millones vía Postgres embebido) + aporte de GPU a la red de traducción.

Fase 6 — Distribución y Tiendas (2–3 semanas)

  • Firmas de producción (Windows, Android, iOS).
  • Publicación Play Store + App Store + GitHub Releases.
  • Manifiesto de auto-update y roll-out gradual.
  • Dashboard de métricas de uso (opcional: telemetría anonimizada).
  • Documentación de usuario final por plataforma.
  • Entregable: releases públicas en las 4 plataformas.

Cronograma Total Estimado

Fase Duración Acumulado
0 2–3 sem 3 sem
1 2 sem 5 sem
2 3–4 sem 9 sem
3 4–5 sem 14 sem
4 3–4 sem 18 sem
5 5–8 sem 26 sem
6 2–3 sem 29 sem

~7 meses con un equipo de 2-3 personas (1 full-stack React/TS, 1 Go/backend, 0.5 Rust/mobile). El MVP (Fases 0-2) se puede alcanzar en ~2 meses.


14. Riesgos y Mitigaciones

# Riesgo Probabilidad Impacto Mitigación
1 Empaquetado de CTranslate2/Python en desktop (dependencias .so, torch cu118) Alta Alto Empezar con PyInstaller sidecar; roadmap hacia port Rust ctranslate2-rs; documentar el patchelf --clear-execstack existente.
2 Políticas de tiendas (iOS/Android) por descarga de modelos grandes Media Alto Modelos on-demand con consentimiento; nunca en el APK; Capa 0 (sin IA local) como default móvil.
3 Coste de mantenimiento de 3 shells (web, Tauri, Capacitor) Alta Medio Mantener una sola SPA; los shells son delgados; abstracciones platform/ estrictas; tests de contrato.
4 JWT/sesión en móvil (24h fija, sin refresh) Alta Medio Fase 1 implementa refresh tokens; SecureStorage para los tokens.
5 SQLite ≠ PostgreSQL en el núcleo embebido Baja (decisión: usar PostgreSQL embebido) Bajo El núcleo usa PostgreSQL portable con el esquema actual intacto; SQLite queda solo como caché de cliente acotada. Si se añade "modo limitado" SQLite, llevar techo duro documentado + ruta de exportación a Postgres.
6 Favoritos sin backend (localStorage hoy) Alta Baja Fase 1 añade endpoints; migración de datos local en primer arranque.
7 Notificaciones push requieren backend nuevo (FCM/APNs) Media Medio Worker Go de push; empezar por alertas; degradar a notificaciones locales en desktop.
8 Rendimiento del WebView en dispositivos Android baratos Media Medio Caché SQLite + infinite scroll; reducir tamaño de bundle (code-splitting); limitar tarjetas cargadas.
9 Seguridad (almacenar refresh token, pinning) Media Alta Keychain/Keystore/Keyring; HTTPS obligatorio; opción de pinning; auditoría de secrets.
10 Traducción de toda la UI (i18n) retrasa fases Media Baja Empezar i18n en Fase 0 (ES como default), EN incremental.
11 Escasez de expertise Rust/mobile Media Media Usar Capacitor (JS puro, sin Swift/Kotlin deep); Rust mínimo en Tauri; plantilla de Tauri como base.
12 SemanticSearch stub — expectativa de búsqueda semántica en la app Media Medio Implementarla en servidor (Fase 1 opcional) o desactivar el toggle en la app.

15. Estrategia de Testing y QA

15.1 Niveles de Testing

Nivel Stack Alcance
Unit (TS) vitest + Testing Library api-client, storage, sync, componentes, hooks
Unit (Go) go test nuevos endpoints (favorites, refresh, push), helpers
Integration API vitest + msw / Go httptest contratos api-client ↔ backend
E2E web Playwright flujos principales (login, home, search, favorites)
E2E desktop Playwright con WebView (Tauri driver) humo por plataforma
E2E móvil Detox / Maestro (emulador) login, offline, push (móvil)
Visual Percy/regression 4 plataformas, light/dark
Rendimiento Lighthouse (web) / bundle-size budget CI gate: bundle < 250KB gzip core

15.2 Cobertura Mínima Obligatoria

  1. api-client: cada endpoint con mock (éxito, 401→refresh→retry, 401→logout, errores de red).
  2. storage: round-trip, corrupción, rotación de versión.
  3. sync: outbox vacío, envío, fallo→reintento, conflictos.
  4. favorites: flujo local→sync→servidor.
  5. Componentes clave: Layout, ProtectedRoute, tarjetas de noticia, WikiTooltip táctil.

15.3 Testing Manual por Plataforma (checklist)

  • Windows: instalación NSIS, auto-update, notificaciones, tray, WebView2 ausente→instalar, SmartScreen.
  • Linux: AppImage/deb en Ubuntu+Debian+Fedora, webkit2gtk instalado, escalado HiDPI.
  • Android: AAB instalado, push FCM, permiso notificaciones, orientación, modo offline/avión, background.
  • iOS: TestFlight, push APNs, permiso notificaciones, Safe Area, Dark Mode.

16. Referencias y Documentación

  • README.md — Guía de despliegue completo del sistema actual.
  • DEPLOY.md — Instrucciones de producción.
  • AGENTS.md — Guía de desarrollo del repositorio.
  • remote-worker.md — Protocolo de workers remotos (base para el módulo worker desktop).
  • SECURITY_GUIDE.md — Guía de seguridad (a revisar para almacenamiento de tokens).
  • QUICKSTART_LLM.md — Configuración LLM.
  • Ficheros clave del código analizado:
    • docker-compose.yml — 20 servicios, topología de redes, recursos.
    • backend/cmd/server/main.go — arranque, rutas, migraciones idempotentes.
    • backend/internal/handlers/*.go — toda la superficie de API.
    • backend/internal/auth/jwt.go — JWT (24h hardcode).
    • backend/internal/services/ml.go — clientes ML (SemanticSearch stub).
    • frontend/src/services/api.ts — contrato API + interceptor 401.
    • frontend/src/App.tsx, main.tsx — boot y routing.
    • frontend/src/pages/*.tsx — las 18 páginas.
    • workers/remote_translator_worker.py — prototipo del modo worker desktop.
    • workers/ctranslator_worker.py, embeddings_worker.py, ner_worker.py — pipelines IA.
    • rss-ingestor-go/main.go — ingesta con politeness y ETag.
    • init-db/*.sql — esquema completo (28 tablas).
    • nginx.conf — gateway.

Apéndice A — Tabla de Correspondencia Frontend↔App

Página actual Estado Acción en la app
Home ✅ Port directo + infinite scroll
News ✅ Port + favorito offline
Search ⚠️ Corregir filtros muertos; semántica condicional
Feeds ✅ CRUD (solo desktop/admin)
Favorites ⚠️ Migrar de localStorage a sync
Account ⚠️ Implementar persistencia real
Login ✅ Añadir refresh tokens
Populares ✅ Port
Analisis ✅ Chart responsivo
Alertas ✅ + push notifications
WelcomeWizard ✅ + wizard "configurar servidor local" (Fase 5)
AdminAliases/Users/Settings/Workers ⚠️ Ocultar/adaptar acciones dependientes de Docker en móvil

Apéndice B — Glosario

  • Sidecar: binario adicional empaquetado junto a la app y lanzado por el shell (ej. el backend Go embebido).
  • Offline-first: diseño donde la app funciona sin red usando datos locales y sincroniza después.
  • Outbox: cola local de escrituras pendientes de confirmar por el servidor.
  • Capa 0/1/2 de IA: modelo de capacidades de IA según el dispositivo (cliente puro / worker GPU / núcleo local).
  • FTS5: módulo de búsqueda full-text de SQLite (equivalente al tsvector de Postgres).