# 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](#1-resumen-ejecutivo) 2. [Análisis Profundo del Código Actual](#2-análisis-profundo-del-código-actual) 3. [Objetivos y Requisitos de la Nueva Aplicación](#3-objetivos-y-requisitos-de-la-nueva-aplicación) 4. [Arquitectura Objetivo (Alternativas)](#4-arquitectura-objetivo-alternativas) 5. [Selección Tecnológica por Plataforma](#5-selección-tecnológica-por-plataforma) 6. [Estrategia de Datos y Sincronización](#6-estrategia-de-datos-y-sincronización) 7. [Estrategia de IA/ML en el Dispositivo](#7-estrategia-de-iaml-en-el-dispositivo) 8. [Núcleo Embebido (Embedded Backend)](#8-núcleo-embebido-embedded-backend) 9. [Rediseño de la API para Clientes Nativos](#9-rediseño-de-la-api-para-clientes-nativos) 10. [Migración del Frontend Paso a Paso](#10-migración-del-frontend-paso-a-paso) 11. [Empaquetado y Distribución por Plataforma](#11-empaquetado-y-distribución-por-plataforma) 12. [Infraestructura de Build y CI/CD](#12-infraestructura-de-build-y-cicd) 13. [Plan de Implementación por Fases](#13-plan-de-implementación-por-fases) 14. [Riesgos y Mitigaciones](#14-riesgos-y-mitigaciones) 15. [Estrategia de Testing y QA](#15-estrategia-de-testing-y-qa) 16. [Referencias y Documentación](#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` + `` | 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](https://capgo.app) 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) ```sql -- 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=` (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`+`` 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=` | 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) ```yaml 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](./README.md) — Guía de despliegue completo del sistema actual. - [DEPLOY.md](./DEPLOY.md) — Instrucciones de producción. - [AGENTS.md](./AGENTS.md) — Guía de desarrollo del repositorio. - [remote-worker.md](./remote-worker.md) — Protocolo de workers remotos (base para el módulo worker desktop). - [SECURITY_GUIDE.md](./SECURITY_GUIDE.md) — Guía de seguridad (a revisar para almacenamiento de tokens). - [QUICKSTART_LLM.md](./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).