1154 lines
71 KiB
Markdown
1154 lines
71 KiB
Markdown
# 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` + `<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](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=<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)
|
||
|
||
```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).
|